@vellumai/assistant 0.6.5 → 0.7.0

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 (1520) hide show
  1. package/AGENTS.md +29 -1
  2. package/ARCHITECTURE.md +60 -53
  3. package/Dockerfile +25 -3
  4. package/README.md +8 -10
  5. package/__tests__/permissions/gateway-threshold-reader.test.ts +277 -0
  6. package/bun.lock +306 -119
  7. package/docs/architecture/integrations.md +32 -39
  8. package/docs/architecture/memory.md +26 -120
  9. package/docs/architecture/security.md +22 -36
  10. package/docs/browser-use-architecture-phase2.md +63 -20
  11. package/docs/credential-execution-service.md +7 -5
  12. package/docs/plugins.md +761 -0
  13. package/docs/skills.md +10 -10
  14. package/docs/stt-provider-onboarding.md +17 -45
  15. package/examples/plugins/echo/README.md +132 -0
  16. package/examples/plugins/echo/bun.lock +25 -0
  17. package/examples/plugins/echo/package.json +17 -0
  18. package/examples/plugins/echo/register.ts +187 -0
  19. package/knip.json +8 -22
  20. package/node_modules/@vellumai/ces-client/bun.lock +33 -0
  21. package/node_modules/@vellumai/ces-client/package.json +25 -0
  22. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +631 -0
  23. package/node_modules/@vellumai/ces-client/src/__tests__/package-boundary.test.ts +138 -0
  24. package/node_modules/@vellumai/ces-client/src/credential-rpc.ts +13 -0
  25. package/node_modules/@vellumai/ces-client/src/http-credentials.ts +296 -0
  26. package/node_modules/@vellumai/ces-client/src/http-log-export.ts +111 -0
  27. package/node_modules/@vellumai/ces-client/src/index.ts +43 -0
  28. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +445 -0
  29. package/node_modules/@vellumai/credential-storage/src/__tests__/package-boundary.test.ts +32 -6
  30. package/node_modules/@vellumai/egress-proxy/src/__tests__/package-boundary.test.ts +32 -1
  31. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  32. package/node_modules/@vellumai/gateway-client/bun.lock +39 -0
  33. package/node_modules/@vellumai/gateway-client/package.json +23 -0
  34. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +343 -0
  35. package/node_modules/@vellumai/gateway-client/src/__tests__/package-boundary.test.ts +140 -0
  36. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +422 -0
  37. package/node_modules/@vellumai/gateway-client/src/index.ts +35 -0
  38. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +331 -0
  39. package/node_modules/@vellumai/gateway-client/src/types.ts +131 -0
  40. package/node_modules/@vellumai/gateway-client/tsconfig.json +20 -0
  41. package/node_modules/@vellumai/{ces-contracts → service-contracts}/bun.lock +1 -1
  42. package/node_modules/@vellumai/{ces-contracts → service-contracts}/package.json +4 -2
  43. package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/__tests__/contracts.test.ts +5 -1
  44. package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +155 -0
  45. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +23 -0
  46. package/node_modules/@vellumai/service-contracts/src/index.ts +25 -0
  47. package/node_modules/@vellumai/{ces-contracts/src/index.ts → service-contracts/src/transport.ts} +6 -28
  48. package/node_modules/@vellumai/service-contracts/src/trust-rules.ts +116 -0
  49. package/node_modules/@vellumai/service-contracts/tsconfig.json +20 -0
  50. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +891 -0
  51. package/node_modules/@vellumai/skill-host-contracts/bun.lock +24 -0
  52. package/node_modules/@vellumai/skill-host-contracts/package.json +18 -0
  53. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +91 -0
  54. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +1348 -0
  55. package/node_modules/@vellumai/skill-host-contracts/src/index.ts +6 -0
  56. package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +11 -0
  57. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +32 -0
  58. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +333 -0
  59. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +444 -0
  60. package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +20 -0
  61. package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +12 -0
  62. package/openapi.yaml +3135 -692
  63. package/package.json +13 -7
  64. package/scripts/check-circular-deps.ts +80 -0
  65. package/scripts/generate-openapi.ts +24 -7
  66. package/{src/memory/graph/inspect.ts → scripts/memory-inspect.ts} +28 -28
  67. package/src/__tests__/access-request-decision.test.ts +2 -11
  68. package/src/__tests__/acp-session.test.ts +4 -150
  69. package/src/__tests__/actor-token-service.test.ts +17 -678
  70. package/src/__tests__/agent-loop-callsite-precedence.test.ts +2 -6
  71. package/src/__tests__/agent-loop-override-profile.test.ts +404 -0
  72. package/src/__tests__/agent-loop-thinking.test.ts +4 -4
  73. package/src/__tests__/agent-wake-override-profile.test.ts +261 -0
  74. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -1
  75. package/src/__tests__/anthropic-provider.test.ts +127 -15
  76. package/src/__tests__/app-compiler.test.ts +57 -0
  77. package/src/__tests__/app-routes-csp.test.ts +106 -55
  78. package/src/__tests__/approval-cascade.test.ts +10 -357
  79. package/src/__tests__/approval-conversation-turn.test.ts +3 -8
  80. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +1 -1
  81. package/src/__tests__/approval-primitive.test.ts +2 -1
  82. package/src/__tests__/approval-routes-http.test.ts +34 -451
  83. package/src/__tests__/assistant-events-sse-hardening.test.ts +73 -80
  84. package/src/__tests__/assistant-id-boundary-guard.test.ts +0 -3
  85. package/src/__tests__/attachment-upload-trusted-source.test.ts +139 -0
  86. package/src/__tests__/attachments-store.test.ts +46 -1
  87. package/src/__tests__/audit-log-rotation.test.ts +2 -1
  88. package/src/__tests__/auto-analysis-end-to-end.test.ts +9 -20
  89. package/src/__tests__/avatar-generator.test.ts +4 -2
  90. package/src/__tests__/background-shell-bash.test.ts +227 -0
  91. package/src/__tests__/background-shell-host-bash.test.ts +474 -0
  92. package/src/__tests__/background-tool-registry.test.ts +145 -0
  93. package/src/__tests__/background-tool-routes.test.ts +175 -0
  94. package/src/__tests__/btw-routes.test.ts +147 -183
  95. package/src/__tests__/bundled-asset.test.ts +6 -6
  96. package/src/__tests__/call-controller.test.ts +15 -2
  97. package/src/__tests__/call-conversation-messages.test.ts +2 -1
  98. package/src/__tests__/call-domain.test.ts +2 -2
  99. package/src/__tests__/call-pointer-messages.test.ts +11 -13
  100. package/src/__tests__/call-recovery.test.ts +2 -1
  101. package/src/__tests__/call-routes-http.test.ts +3 -14
  102. package/src/__tests__/call-store.test.ts +2 -1
  103. package/src/__tests__/cancel-resolves-conversation-key.test.ts +31 -62
  104. package/src/__tests__/canonical-guardian-store.test.ts +2 -2
  105. package/src/__tests__/catalog-cache.test.ts +69 -0
  106. package/src/__tests__/catalog-files.test.ts +0 -26
  107. package/src/__tests__/ces-rpc-credential-backend.test.ts +1 -1
  108. package/src/__tests__/channel-approval-routes.test.ts +79 -49
  109. package/src/__tests__/channel-approval.test.ts +9 -7
  110. package/src/__tests__/channel-approvals.test.ts +9 -180
  111. package/src/__tests__/channel-delivery-store.test.ts +11 -10
  112. package/src/__tests__/channel-guardian.test.ts +14 -25
  113. package/src/__tests__/channel-readiness-service.test.ts +8 -6
  114. package/src/__tests__/channel-reply-delivery.test.ts +3 -19
  115. package/src/__tests__/channel-retry-sweep.test.ts +2 -5
  116. package/src/__tests__/checker.test.ts +447 -3806
  117. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  118. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +208 -0
  119. package/src/__tests__/cli.test.ts +1 -38
  120. package/src/__tests__/compaction-events.test.ts +500 -0
  121. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  122. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  123. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  124. package/src/__tests__/config-managed-gemini-defaults.test.ts +3 -7
  125. package/src/__tests__/config-model-image-provider.test.ts +109 -0
  126. package/src/__tests__/config-schema-cmd.test.ts +1 -1
  127. package/src/__tests__/config-schema.test.ts +25 -203
  128. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  129. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -25
  130. package/src/__tests__/contact-store-user-file.test.ts +2 -1
  131. package/src/__tests__/contacts-tools.test.ts +71 -18
  132. package/src/__tests__/contacts-write.test.ts +6 -61
  133. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  134. package/src/__tests__/context-search-agent-protocol.test.ts +230 -0
  135. package/src/__tests__/context-search-agent-runner.test.ts +998 -0
  136. package/src/__tests__/context-search-conversations-source.test.ts +320 -0
  137. package/src/__tests__/context-search-fanout.test.ts +380 -0
  138. package/src/__tests__/context-search-memory-source.test.ts +311 -0
  139. package/src/__tests__/context-search-pkb-source.test.ts +444 -0
  140. package/src/__tests__/context-search-types.test.ts +95 -0
  141. package/src/__tests__/context-search-workspace-source.test.ts +545 -0
  142. package/src/__tests__/context-window-manager.test.ts +380 -4
  143. package/src/__tests__/conversation-abort-tool-results.test.ts +14 -2
  144. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +631 -0
  145. package/src/__tests__/conversation-agent-loop-overflow.test.ts +41 -32
  146. package/src/__tests__/conversation-agent-loop.test.ts +54 -143
  147. package/src/__tests__/conversation-analysis-routes.test.ts +60 -82
  148. package/src/__tests__/conversation-attachments.test.ts +9 -20
  149. package/src/__tests__/conversation-attention-store.test.ts +2 -1
  150. package/src/__tests__/conversation-attention-telegram.test.ts +4 -2
  151. package/src/__tests__/conversation-clear-safety.test.ts +53 -95
  152. package/src/__tests__/conversation-confirmation-signals.test.ts +7 -40
  153. package/src/__tests__/conversation-crud-inference-profile.test.ts +54 -0
  154. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +63 -157
  155. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  156. package/src/__tests__/conversation-disk-view.test.ts +5 -4
  157. package/src/__tests__/conversation-fork-crud.test.ts +26 -55
  158. package/src/__tests__/conversation-fork-route.test.ts +5 -74
  159. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  160. package/src/__tests__/conversation-inference-profile-list.test.ts +128 -0
  161. package/src/__tests__/conversation-inference-profile-route.test.ts +216 -0
  162. package/src/__tests__/conversation-init.benchmark.test.ts +4 -95
  163. package/src/__tests__/conversation-key-store-disk-view.test.ts +2 -1
  164. package/src/__tests__/conversation-lifecycle.test.ts +0 -1
  165. package/src/__tests__/conversation-list-source.test.ts +2 -2
  166. package/src/__tests__/conversation-load-history-repair.test.ts +0 -1
  167. package/src/__tests__/conversation-pairing.test.ts +174 -11
  168. package/src/__tests__/conversation-pre-run-repair.test.ts +137 -294
  169. package/src/__tests__/conversation-process-callsite.test.ts +3 -1
  170. package/src/__tests__/conversation-provider-retry-repair.test.ts +22 -8
  171. package/src/__tests__/conversation-queue.test.ts +30 -47
  172. package/src/__tests__/conversation-routes-disk-view.test.ts +131 -103
  173. package/src/__tests__/conversation-routes-guardian-reply.test.ts +80 -55
  174. package/src/__tests__/conversation-routes-slash-commands.test.ts +83 -12
  175. package/src/__tests__/conversation-runtime-assembly.test.ts +196 -194
  176. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  177. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  178. package/src/__tests__/conversation-slash-commands.test.ts +6 -43
  179. package/src/__tests__/conversation-slash-queue.test.ts +7 -3
  180. package/src/__tests__/conversation-slash-unknown.test.ts +25 -3
  181. package/src/__tests__/conversation-speed-override.test.ts +6 -2
  182. package/src/__tests__/conversation-starter-routes.test.ts +177 -55
  183. package/src/__tests__/conversation-starters-cadence.test.ts +2 -2
  184. package/src/__tests__/conversation-store.test.ts +2 -375
  185. package/src/__tests__/conversation-title-service.test.ts +116 -0
  186. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +42 -3
  187. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +6 -6
  188. package/src/__tests__/conversation-unread-route.test.ts +1 -1
  189. package/src/__tests__/conversation-usage.test.ts +3 -2
  190. package/src/__tests__/conversation-wipe.test.ts +2 -103
  191. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -2
  192. package/src/__tests__/conversation-workspace-injection.test.ts +3 -1
  193. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -2
  194. package/src/__tests__/conversations-defer-cli.test.ts +150 -0
  195. package/src/__tests__/credential-execution-admin-cli.test.ts +1 -1
  196. package/src/__tests__/credential-execution-api-key-propagation.test.ts +2 -2
  197. package/src/__tests__/credential-execution-approval-bridge.test.ts +22 -289
  198. package/src/__tests__/credential-execution-client.test.ts +1 -1
  199. package/src/__tests__/credential-execution-managed-contract.test.ts +1 -1
  200. package/src/__tests__/credential-health-service.test.ts +78 -9
  201. package/src/__tests__/credential-security-invariants.test.ts +16 -2
  202. package/src/__tests__/credentials-cli.test.ts +45 -21
  203. package/src/__tests__/daemon-credential-client.test.ts +23 -108
  204. package/src/__tests__/db-acp-history.test.ts +284 -0
  205. package/src/__tests__/db-activation-state.test.ts +240 -0
  206. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +2 -1
  207. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +248 -0
  208. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +2 -1
  209. package/src/__tests__/db-memory-graph-event-date-repair.test.ts +116 -0
  210. package/src/__tests__/db-rename-inference-profile-snake-case-migration.test.ts +132 -0
  211. package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
  212. package/src/__tests__/delete-propagation.test.ts +3 -2
  213. package/src/__tests__/deterministic-verification-control-plane.test.ts +39 -32
  214. package/src/__tests__/dm-backfill.test.ts +3 -2
  215. package/src/__tests__/edit-propagation.test.ts +5 -7
  216. package/src/__tests__/embedding-managed-proxy-selection.test.ts +1 -1
  217. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  218. package/src/__tests__/events-client-registration.test.ts +297 -0
  219. package/src/__tests__/file-write-tool.test.ts +2 -4
  220. package/src/__tests__/filing-service.test.ts +144 -17
  221. package/src/__tests__/first-greeting.test.ts +247 -5
  222. package/src/__tests__/followup-tools.test.ts +2 -1
  223. package/src/__tests__/gateway-client-managed-outbound.test.ts +8 -12
  224. package/src/__tests__/gateway-only-enforcement.test.ts +2 -6
  225. package/src/__tests__/gateway-only-guard.test.ts +4 -3
  226. package/src/__tests__/gemini-provider.test.ts +276 -10
  227. package/src/__tests__/graph-extraction-event-date.test.ts +30 -0
  228. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -1
  229. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -2
  230. package/src/__tests__/guardian-action-followup-store.test.ts +2 -1
  231. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +9 -9
  232. package/src/__tests__/guardian-action-late-reply.test.ts +2 -1
  233. package/src/__tests__/guardian-action-store.test.ts +2 -1
  234. package/src/__tests__/guardian-action-sweep.test.ts +9 -8
  235. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -1
  236. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +21 -118
  237. package/src/__tests__/guardian-dispatch.test.ts +14 -11
  238. package/src/__tests__/guardian-grant-minting.test.ts +9 -15
  239. package/src/__tests__/guardian-outbound-http.test.ts +71 -106
  240. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -2
  241. package/src/__tests__/guardian-routing-invariants.test.ts +34 -90
  242. package/src/__tests__/guardian-routing-state.test.ts +14 -22
  243. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -2
  244. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +253 -0
  245. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +8 -4
  246. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  247. package/src/__tests__/heartbeat-service.test.ts +39 -21
  248. package/src/__tests__/helpers/call-route-handler.ts +72 -0
  249. package/src/__tests__/helpers/channel-test-adapter.ts +161 -0
  250. package/src/__tests__/helpers/gateway-classify-mock.ts +67 -0
  251. package/src/__tests__/helpers/mock-logger.ts +36 -0
  252. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  253. package/src/__tests__/home-state-routes.test.ts +10 -31
  254. package/src/__tests__/host-browser-e2e-cloud.test.ts +309 -1
  255. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +12 -2
  256. package/src/__tests__/host-browser-routes.test.ts +36 -91
  257. package/src/__tests__/host-browser-ws-events-e2e.test.ts +10 -2
  258. package/src/__tests__/host-proxy-interface.test.ts +38 -4
  259. package/src/__tests__/host-shell-tool.test.ts +2 -4
  260. package/src/__tests__/host-transfer-pending-interactions.test.ts +160 -0
  261. package/src/__tests__/host-transfer-proxy.test.ts +733 -0
  262. package/src/__tests__/http-conversation-lineage.test.ts +3 -2
  263. package/src/__tests__/http-user-message-parity.test.ts +20 -11
  264. package/src/__tests__/image-credentials.test.ts +137 -0
  265. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  266. package/src/__tests__/inbound-invite-redemption.test.ts +3 -2
  267. package/src/__tests__/injector-chain.test.ts +525 -0
  268. package/src/__tests__/inline-skill-load-permissions.test.ts +41 -206
  269. package/src/__tests__/install-skill-routing.test.ts +1 -1
  270. package/src/__tests__/intent-routing.test.ts +0 -26
  271. package/src/__tests__/invite-redemption-service.test.ts +2 -1
  272. package/src/__tests__/invite-routes-http.test.ts +80 -12
  273. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +2 -1
  274. package/src/__tests__/jobs-store-upsert-debounced.test.ts +2 -1
  275. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +157 -0
  276. package/src/__tests__/list-messages-attachments.test.ts +52 -55
  277. package/src/__tests__/list-messages-page-latest.test.ts +283 -0
  278. package/src/__tests__/list-messages-tool-merge.test.ts +16 -17
  279. package/src/__tests__/llm-call-pipeline.test.ts +284 -0
  280. package/src/__tests__/llm-context-normalization.test.ts +69 -4
  281. package/src/__tests__/llm-context-route-provider.test.ts +39 -113
  282. package/src/__tests__/llm-request-log-turn-query.test.ts +2 -1
  283. package/src/__tests__/llm-resolver.test.ts +211 -0
  284. package/src/__tests__/llm-schema.test.ts +56 -0
  285. package/src/__tests__/llm-usage-store.test.ts +2 -1
  286. package/src/__tests__/log-export-workspace.test.ts +28 -17
  287. package/src/__tests__/mcp-abort-signal.test.ts +2 -3
  288. package/src/__tests__/mcp-client-auth.test.ts +2 -3
  289. package/src/__tests__/media-generate-image.test.ts +119 -13
  290. package/src/__tests__/memory-admin-recall.test.ts +221 -0
  291. package/src/__tests__/memory-recall-log-store.test.ts +2 -1
  292. package/src/__tests__/memory-retrieval-pipeline.test.ts +399 -0
  293. package/src/__tests__/memory-upsert-concurrency.test.ts +3 -1
  294. package/src/__tests__/migration-cross-version-compatibility.test.ts +14 -13
  295. package/src/__tests__/migration-export-http.test.ts +17 -17
  296. package/src/__tests__/migration-export-to-gcs.test.ts +491 -0
  297. package/src/__tests__/migration-import-commit-http.test.ts +16 -16
  298. package/src/__tests__/migration-import-from-gcs.test.ts +533 -0
  299. package/src/__tests__/migration-import-from-url.test.ts +21 -91
  300. package/src/__tests__/migration-import-preflight-http.test.ts +13 -13
  301. package/src/__tests__/migration-jobs-status.test.ts +164 -0
  302. package/src/__tests__/migration-validate-http.test.ts +48 -83
  303. package/src/__tests__/mock-gateway-ipc.ts +32 -62
  304. package/src/__tests__/model-intents.test.ts +16 -1
  305. package/src/__tests__/nl-approval-parser.test.ts +13 -17
  306. package/src/__tests__/non-member-access-request.test.ts +13 -5
  307. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  308. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  309. package/src/__tests__/notification-guardian-path.test.ts +15 -8
  310. package/src/__tests__/notification-schedule-notify-dedup.test.ts +109 -0
  311. package/src/__tests__/notification-telegram-adapter.test.ts +57 -55
  312. package/src/__tests__/oauth-apps-routes.test.ts +77 -123
  313. package/src/__tests__/oauth-cli.test.ts +28 -13
  314. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  315. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  316. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  317. package/src/__tests__/oauth-provider-visibility.test.ts +6 -6
  318. package/src/__tests__/oauth-providers-routes.test.ts +81 -103
  319. package/src/__tests__/oauth-store.test.ts +44 -77
  320. package/src/__tests__/oauth2-gateway-transport.test.ts +6 -3
  321. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  322. package/src/__tests__/openai-image-service.test.ts +368 -0
  323. package/src/__tests__/openai-provider.test.ts +105 -6
  324. package/src/__tests__/openai-responses-provider.test.ts +146 -4
  325. package/src/__tests__/openrouter-provider-only.test.ts +22 -4
  326. package/src/__tests__/overflow-reduce-pipeline.test.ts +671 -0
  327. package/src/__tests__/permission-types.test.ts +3 -18
  328. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  329. package/src/__tests__/persistence-pipeline.test.ts +378 -0
  330. package/src/__tests__/pipeline-runner.test.ts +565 -0
  331. package/src/__tests__/platform-bash-auto-approve.test.ts +27 -20
  332. package/src/__tests__/platform.test.ts +10 -59
  333. package/src/__tests__/playbook-execution.test.ts +2 -1
  334. package/src/__tests__/playbook-tools.test.ts +2 -1
  335. package/src/__tests__/plugin-bootstrap.test.ts +529 -0
  336. package/src/__tests__/plugin-registry.test.ts +303 -0
  337. package/src/__tests__/plugin-route-contribution.test.ts +294 -0
  338. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  339. package/src/__tests__/plugin-tool-contribution.test.ts +292 -0
  340. package/src/__tests__/plugin-types.test.ts +320 -0
  341. package/src/__tests__/pricing.test.ts +195 -14
  342. package/src/__tests__/profiler-routes.test.ts +112 -177
  343. package/src/__tests__/provider-send-message-override-profile.test.ts +223 -0
  344. package/src/__tests__/proxy-approval-callback.test.ts +6 -493
  345. package/src/__tests__/qdrant-collection-migration.test.ts +7 -7
  346. package/src/__tests__/reaction-persistence.test.ts +4 -2
  347. package/src/__tests__/rebuild-index-graph-nodes.test.ts +1 -1
  348. package/src/__tests__/recording-handler.test.ts +0 -2
  349. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  350. package/src/__tests__/registry.test.ts +1 -2
  351. package/src/__tests__/relay-server.test.ts +19 -4
  352. package/src/__tests__/require-fresh-approval.test.ts +19 -168
  353. package/src/__tests__/resolve-trust-class.test.ts +2 -1
  354. package/src/__tests__/retry-thinking-tool-choice.test.ts +19 -7
  355. package/src/__tests__/retry-verbosity-normalization.test.ts +139 -0
  356. package/src/__tests__/runtime-attachment-metadata.test.ts +26 -6
  357. package/src/__tests__/runtime-events-sse-parity.test.ts +12 -13
  358. package/src/__tests__/runtime-events-sse.test.ts +13 -21
  359. package/src/__tests__/schedule-routes.test.ts +304 -77
  360. package/src/__tests__/schedule-store.test.ts +119 -1
  361. package/src/__tests__/schedule-tools.test.ts +2 -1
  362. package/src/__tests__/scheduler-recurrence.test.ts +16 -71
  363. package/src/__tests__/scheduler-reuse-conversation.test.ts +12 -51
  364. package/src/__tests__/scheduler-wake.test.ts +356 -0
  365. package/src/__tests__/scoped-approval-grants.test.ts +2 -1
  366. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -1
  367. package/src/__tests__/secret-detection-handler.test.ts +2 -19
  368. package/src/__tests__/secret-ingress-http.test.ts +38 -21
  369. package/src/__tests__/secret-routes-managed-proxy.test.ts +46 -102
  370. package/src/__tests__/secret-scanner-executor.test.ts +1 -2
  371. package/src/__tests__/send-endpoint-busy.test.ts +38 -25
  372. package/src/__tests__/sequence-store.test.ts +2 -1
  373. package/src/__tests__/server-history-render.test.ts +2 -2
  374. package/src/__tests__/service-contracts-import-guard.test.ts +185 -0
  375. package/src/__tests__/set-permission-mode.test.ts +0 -10
  376. package/src/__tests__/settings-routes.test.ts +35 -68
  377. package/src/__tests__/skill-boundary-guard.test.ts +105 -0
  378. package/src/__tests__/skill-load-inline-command.test.ts +2 -2
  379. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  380. package/src/__tests__/skill-runtime-path.test.ts +64 -0
  381. package/src/__tests__/skills-file-content-endpoint.test.ts +0 -2
  382. package/src/__tests__/slack-inbound-verification.test.ts +11 -2
  383. package/src/__tests__/slack-messaging-token-resolution.test.ts +1 -3
  384. package/src/__tests__/slack-reaction-approvals.test.ts +4 -4
  385. package/src/__tests__/slack-share-routes.test.ts +37 -72
  386. package/src/__tests__/subagent-call-site-routing.test.ts +79 -0
  387. package/src/__tests__/subagent-fork-spawn.test.ts +20 -28
  388. package/src/__tests__/subagent-notify-parent.test.ts +6 -29
  389. package/src/__tests__/subagent-role-registry.test.ts +3 -3
  390. package/src/__tests__/subagent-spawn-tool-fork.test.ts +52 -104
  391. package/src/__tests__/subagent-tools.test.ts +0 -1
  392. package/src/__tests__/suggestion-routes.test.ts +149 -57
  393. package/src/__tests__/task-compiler.test.ts +2 -1
  394. package/src/__tests__/task-management-tools.test.ts +2 -1
  395. package/src/__tests__/task-memory-cleanup.test.ts +3 -1
  396. package/src/__tests__/task-scheduler.test.ts +5 -16
  397. package/src/__tests__/telegram-config.test.ts +0 -1
  398. package/src/__tests__/terminal-tools.test.ts +5 -314
  399. package/src/__tests__/thread-backfill.test.ts +3 -2
  400. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  401. package/src/__tests__/token-estimate-pipeline.test.ts +484 -0
  402. package/src/__tests__/tool-approval-handler.test.ts +21 -63
  403. package/src/__tests__/tool-audit-listener.test.ts +3 -3
  404. package/src/__tests__/tool-domain-event-publisher.test.ts +3 -3
  405. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  406. package/src/__tests__/tool-execute-pipeline.test.ts +429 -0
  407. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +61 -4
  408. package/src/__tests__/tool-executor-lifecycle-events.test.ts +28 -56
  409. package/src/__tests__/tool-executor.test.ts +434 -1604
  410. package/src/__tests__/tool-grant-request-escalation.test.ts +90 -311
  411. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  412. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  413. package/src/__tests__/trust-context-guards.test.ts +1 -1
  414. package/src/__tests__/trusted-contact-approval-notifier.test.ts +7 -15
  415. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +178 -354
  416. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +3 -2
  417. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -2
  418. package/src/__tests__/trusted-contact-verification.test.ts +2 -1
  419. package/src/__tests__/turn-boundary-resolution.test.ts +2 -1
  420. package/src/__tests__/twilio-routes.test.ts +25 -66
  421. package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -7
  422. package/src/__tests__/usage-routes.test.ts +73 -90
  423. package/src/__tests__/user-plugin-loader.test.ts +233 -0
  424. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -2
  425. package/src/__tests__/verification-control-plane-policy.test.ts +95 -14
  426. package/src/__tests__/voice-ingress-preflight.test.ts +5 -5
  427. package/src/__tests__/voice-invite-redemption.test.ts +2 -1
  428. package/src/__tests__/voice-scoped-grant-consumer.test.ts +3 -3
  429. package/src/__tests__/voice-session-bridge.test.ts +285 -106
  430. package/src/__tests__/volume-security-guard.test.ts +0 -2
  431. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -1
  432. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +3 -1
  433. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +2 -1
  434. package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +1 -1
  435. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  436. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  437. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  438. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  439. package/src/__tests__/workspace-migration-052-seed-default-inference-profiles.test.ts +260 -0
  440. package/src/__tests__/workspace-migration-053-release-notes-acp-codex.test.ts +225 -0
  441. package/src/__tests__/workspace-migration-054-seed-recall-callsite.test.ts +235 -0
  442. package/src/__tests__/workspace-migration-055-release-notes-agentic-recall.test.ts +128 -0
  443. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +232 -0
  444. package/src/__tests__/workspace-migration-acp-sessions-ui.test.ts +144 -0
  445. package/src/__tests__/workspace-migration-drop-user-md.test.ts +1 -1
  446. package/src/__tests__/workspace-migration-memory-v2-init.test.ts +274 -0
  447. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  448. package/src/__tests__/workspace-policy.test.ts +21 -3
  449. package/src/acp/__tests__/client-handler.test.ts +64 -0
  450. package/src/acp/__tests__/helpers/acp-config-stub.ts +62 -0
  451. package/src/acp/__tests__/helpers/which-stub.ts +45 -0
  452. package/src/acp/__tests__/session-manager-persistence.test.ts +366 -0
  453. package/src/acp/__tests__/session-manager-startup.test.ts +159 -0
  454. package/src/acp/__tests__/session-manager.test.ts +83 -0
  455. package/src/acp/client-handler.ts +23 -139
  456. package/src/acp/resolve-agent.test.ts +291 -0
  457. package/src/acp/resolve-agent.ts +176 -0
  458. package/src/acp/session-manager.ts +166 -7
  459. package/src/acp/types.ts +2 -50
  460. package/src/agent/loop.ts +365 -104
  461. package/src/agent/message-types.ts +0 -2
  462. package/src/approvals/AGENTS.md +1 -1
  463. package/src/approvals/__tests__/guardian-feed-event.test.ts +296 -0
  464. package/src/approvals/approval-primitive.ts +3 -20
  465. package/src/approvals/guardian-decision-primitive.ts +37 -68
  466. package/src/approvals/guardian-request-resolvers.ts +109 -103
  467. package/src/avatar/character-components.ts +6 -6
  468. package/src/{config/bundled-skills/settings/tools → avatar}/identity-avatar.ts +1 -1
  469. package/src/backup/__tests__/backup-worker.test.ts +2 -15
  470. package/src/backup/__tests__/paths.test.ts +3 -2
  471. package/src/backup/backup-worker.ts +3 -24
  472. package/src/backup/paths.ts +2 -18
  473. package/src/backup/restore.ts +7 -11
  474. package/src/browser/__tests__/operations.test.ts +0 -35
  475. package/src/browser/operations.ts +1 -47
  476. package/src/bundler/app-compiler.ts +84 -1
  477. package/src/bundler/package-resolver.ts +2 -6
  478. package/src/calls/active-call-lease.ts +1 -1
  479. package/src/calls/call-constants.ts +1 -1
  480. package/src/calls/call-controller.ts +1 -5
  481. package/src/calls/call-domain.ts +14 -14
  482. package/src/calls/call-pointer-messages.ts +4 -9
  483. package/src/calls/call-state.ts +2 -2
  484. package/src/calls/call-store.ts +2 -1
  485. package/src/calls/guardian-action-sweep.ts +9 -25
  486. package/src/calls/guardian-dispatch.ts +1 -20
  487. package/src/calls/media-stream-audio-transcode.ts +2 -41
  488. package/src/calls/media-stream-server.ts +2 -3
  489. package/src/calls/media-stream-stt-session.ts +1 -3
  490. package/src/calls/relay-access-wait.ts +5 -8
  491. package/src/calls/relay-server.ts +15 -18
  492. package/src/calls/relay-setup-router.ts +2 -2
  493. package/src/calls/relay-verification.ts +4 -4
  494. package/src/calls/twilio-rest.ts +1 -1
  495. package/src/calls/twilio-routes.ts +160 -78
  496. package/src/calls/voice-control-protocol.ts +10 -10
  497. package/src/calls/voice-ingress-preflight.ts +2 -2
  498. package/src/calls/voice-session-bridge.ts +137 -42
  499. package/src/channels/__tests__/types.test.ts +28 -6
  500. package/src/channels/permission-profiles.ts +2 -72
  501. package/src/channels/types.ts +48 -30
  502. package/src/cli/AGENTS.md +1 -0
  503. package/src/cli/__tests__/notifications.test.ts +92 -214
  504. package/src/cli/commands/__tests__/attachment.test.ts +14 -8
  505. package/src/cli/commands/__tests__/backup.test.ts +4 -15
  506. package/src/cli/commands/__tests__/browser.test.ts +36 -31
  507. package/src/cli/commands/__tests__/cache.test.ts +23 -18
  508. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  509. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  510. package/src/cli/commands/__tests__/memory-v2.test.ts +396 -0
  511. package/src/cli/commands/__tests__/task.test.ts +36 -35
  512. package/src/cli/commands/__tests__/trust.test.ts +602 -0
  513. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  514. package/src/cli/commands/__tests__/ui-confirm.test.ts +14 -14
  515. package/src/cli/commands/__tests__/ui.test.ts +17 -17
  516. package/src/cli/commands/__tests__/watchers.test.ts +29 -29
  517. package/src/cli/commands/__tests__/webhooks.test.ts +544 -0
  518. package/src/cli/commands/attachment.ts +12 -8
  519. package/src/cli/commands/auth.ts +1 -1
  520. package/src/cli/commands/avatar.ts +192 -9
  521. package/src/cli/commands/backup.ts +16 -46
  522. package/src/cli/commands/browser.ts +52 -4
  523. package/src/cli/commands/cache.ts +7 -5
  524. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  525. package/src/cli/commands/clients.ts +137 -0
  526. package/src/cli/commands/completions.ts +3 -10
  527. package/src/cli/commands/contacts.ts +10 -10
  528. package/src/cli/commands/conversations-defer.ts +364 -0
  529. package/src/cli/commands/conversations-import.ts +2 -3
  530. package/src/cli/commands/conversations.ts +115 -57
  531. package/src/cli/commands/credential-execution.ts +1 -1
  532. package/src/cli/commands/credentials.ts +139 -5
  533. package/src/cli/commands/default-action.ts +1 -1
  534. package/src/cli/commands/domain.ts +2 -2
  535. package/src/cli/commands/email.ts +7 -7
  536. package/src/cli/commands/image-generation.ts +33 -34
  537. package/src/cli/commands/keys.ts +2 -2
  538. package/src/cli/commands/mcp.ts +1 -1
  539. package/src/cli/commands/memory-v2.ts +343 -0
  540. package/src/cli/commands/memory.ts +8 -8
  541. package/src/cli/commands/notifications.ts +87 -121
  542. package/src/cli/commands/oauth/__tests__/connect.test.ts +23 -5
  543. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +1 -1
  544. package/src/cli/commands/oauth/__tests__/mode.test.ts +1 -1
  545. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  546. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  547. package/src/cli/commands/oauth/__tests__/status.test.ts +1 -1
  548. package/src/cli/commands/oauth/__tests__/token.test.ts +1 -1
  549. package/src/cli/commands/oauth/connect.ts +4 -4
  550. package/src/cli/commands/oauth/providers.ts +176 -8
  551. package/src/cli/commands/oauth/shared.ts +29 -2
  552. package/src/cli/commands/oauth/status.ts +46 -36
  553. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -6
  554. package/src/cli/commands/platform/__tests__/connect.test.ts +23 -11
  555. package/src/cli/commands/platform/__tests__/disconnect.test.ts +22 -10
  556. package/src/cli/commands/platform/__tests__/status.test.ts +22 -10
  557. package/src/cli/commands/platform/connect.ts +3 -3
  558. package/src/cli/commands/platform/disconnect.ts +4 -6
  559. package/src/cli/commands/platform/index.ts +12 -10
  560. package/src/cli/commands/routes.ts +7 -1
  561. package/src/cli/commands/sequence.ts +7 -7
  562. package/src/cli/commands/skills.ts +189 -84
  563. package/src/cli/commands/task.ts +12 -10
  564. package/src/cli/commands/trust.ts +460 -162
  565. package/src/cli/commands/ui.ts +3 -3
  566. package/src/cli/commands/usage.ts +10 -5
  567. package/src/cli/commands/watchers.ts +8 -8
  568. package/src/cli/commands/webhooks.ts +270 -0
  569. package/src/cli/lib/daemon-avatar-client.ts +37 -0
  570. package/src/cli/lib/daemon-credential-client.ts +27 -189
  571. package/src/cli/lib/ipc-params.ts +22 -0
  572. package/src/cli/program.ts +29 -29
  573. package/src/cli.ts +1 -61
  574. package/src/config/__tests__/backup-schema.test.ts +7 -2
  575. package/src/config/acp-defaults.test.ts +57 -0
  576. package/src/config/acp-defaults.ts +40 -0
  577. package/src/config/acp-schema.ts +1 -1
  578. package/src/config/assistant-feature-flags.ts +18 -142
  579. package/src/config/bundled-skills/acp/SKILL.md +44 -16
  580. package/src/config/bundled-skills/acp/TOOLS.json +45 -1
  581. package/src/config/bundled-skills/{screen-watch/tools/start-screen-watch.ts → acp/tools/acp-list-agents.ts} +2 -2
  582. package/src/config/bundled-skills/acp/tools/acp-steer.ts +12 -0
  583. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  584. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  585. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  586. package/src/config/bundled-skills/contacts/tools/contact-search.ts +25 -51
  587. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +31 -44
  588. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  589. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  590. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  591. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +6 -6
  592. package/src/config/bundled-skills/media-processing/services/reduce.ts +0 -13
  593. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  594. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  595. package/src/config/bundled-skills/messaging/tools/gmail-mime-helpers.ts +1 -1
  596. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  597. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  598. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  599. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -1
  600. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +1 -1
  601. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -1
  602. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  603. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  604. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  605. package/src/config/bundled-skills/settings/SKILL.md +2 -17
  606. package/src/config/bundled-skills/settings/TOOLS.json +0 -56
  607. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  608. package/src/config/bundled-tool-registry.ts +4 -21
  609. package/src/config/env.ts +7 -8
  610. package/src/config/feature-flag-registry.json +25 -17
  611. package/src/config/llm-resolver.ts +51 -33
  612. package/src/config/loader.ts +12 -15
  613. package/src/config/schema.ts +22 -70
  614. package/src/config/schemas/__tests__/filing.test.ts +58 -0
  615. package/src/config/schemas/__tests__/memory-v2.test.ts +186 -0
  616. package/src/config/schemas/backup.ts +1 -1
  617. package/src/config/schemas/conversations.ts +16 -0
  618. package/src/config/schemas/filing.ts +12 -0
  619. package/src/config/schemas/host-browser.ts +2 -2
  620. package/src/config/schemas/inference.ts +0 -2
  621. package/src/config/schemas/ingress.ts +1 -1
  622. package/src/config/schemas/llm.ts +51 -10
  623. package/src/config/schemas/memory-storage.ts +1 -1
  624. package/src/config/schemas/memory-v2.ts +176 -0
  625. package/src/config/schemas/memory.ts +2 -0
  626. package/src/config/schemas/security.ts +0 -60
  627. package/src/config/schemas/services.ts +46 -7
  628. package/src/config/schemas/tts.ts +11 -0
  629. package/src/config/skill-state.ts +6 -2
  630. package/src/config/skills.ts +95 -6
  631. package/src/config/types.ts +0 -41
  632. package/src/contacts/contact-store.ts +2 -2
  633. package/src/contacts/contacts-write.ts +0 -38
  634. package/src/contacts/types.ts +8 -10
  635. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  636. package/src/context/prompts/compact.md +26 -12
  637. package/src/context/token-estimator.ts +1 -1
  638. package/src/context/tool-result-truncation.ts +4 -64
  639. package/src/context/window-manager.ts +191 -17
  640. package/src/credential-execution/approval-bridge.ts +7 -69
  641. package/src/credential-execution/client.ts +17 -422
  642. package/src/credential-execution/feature-gates.ts +1 -2
  643. package/src/credential-execution/managed-catalog.ts +1 -1
  644. package/src/credential-health/credential-health-service.ts +20 -7
  645. package/src/daemon/__tests__/conversation-feed-event.test.ts +304 -0
  646. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  647. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
  648. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  649. package/src/daemon/__tests__/daemon-skill-host.test.ts +272 -0
  650. package/src/daemon/__tests__/meet-host-supervisor.test.ts +587 -0
  651. package/src/daemon/__tests__/meet-manifest-loader.test.ts +463 -0
  652. package/src/daemon/approval-generators.ts +2 -14
  653. package/src/daemon/classifier.ts +0 -106
  654. package/src/daemon/config-watcher.ts +14 -56
  655. package/src/daemon/connection-policy.ts +0 -14
  656. package/src/daemon/context-overflow-policy.ts +4 -13
  657. package/src/daemon/conversation-agent-loop-handlers.ts +120 -28
  658. package/src/daemon/conversation-agent-loop.ts +1113 -701
  659. package/src/daemon/conversation-attachments.ts +5 -81
  660. package/src/daemon/conversation-error.ts +9 -5
  661. package/src/daemon/conversation-history.ts +11 -20
  662. package/src/daemon/conversation-launch.ts +1 -1
  663. package/src/daemon/conversation-lifecycle.ts +37 -19
  664. package/src/daemon/conversation-messaging.ts +1 -1
  665. package/src/daemon/conversation-notifiers.ts +3 -111
  666. package/src/daemon/conversation-process.ts +23 -20
  667. package/src/daemon/conversation-runtime-assembly.ts +530 -471
  668. package/src/daemon/conversation-slash.ts +4 -160
  669. package/src/daemon/conversation-store.ts +368 -0
  670. package/src/daemon/conversation-surfaces.ts +5 -4
  671. package/src/daemon/conversation-tool-setup.ts +49 -161
  672. package/src/daemon/conversation.ts +126 -217
  673. package/src/daemon/daemon-control.ts +3 -3
  674. package/src/daemon/daemon-skill-host.ts +262 -0
  675. package/src/daemon/external-plugins-bootstrap.ts +532 -0
  676. package/src/daemon/first-greeting.ts +191 -14
  677. package/src/daemon/handlers/config-channels.ts +2 -2
  678. package/src/daemon/handlers/config-embeddings.ts +1 -1
  679. package/src/daemon/handlers/config-ingress.ts +24 -2
  680. package/src/daemon/handlers/config-model.test.ts +17 -0
  681. package/src/daemon/handlers/config-model.ts +18 -52
  682. package/src/daemon/handlers/config-telegram.ts +6 -53
  683. package/src/daemon/handlers/config-voice.ts +1 -1
  684. package/src/daemon/handlers/conversations.ts +22 -156
  685. package/src/daemon/handlers/recording.ts +1 -1
  686. package/src/daemon/handlers/shared.ts +34 -35
  687. package/src/daemon/handlers/skills.ts +20 -24
  688. package/src/daemon/host-transfer-proxy.ts +500 -0
  689. package/src/daemon/lifecycle.ts +56 -326
  690. package/src/daemon/meet-host-startup.ts +51 -0
  691. package/src/daemon/meet-host-supervisor.ts +781 -0
  692. package/src/daemon/meet-manifest-loader.ts +410 -0
  693. package/src/daemon/memory-v2-startup.ts +35 -0
  694. package/src/daemon/message-protocol.ts +4 -7
  695. package/src/daemon/message-types/acp.ts +1 -0
  696. package/src/daemon/message-types/computer-use.ts +2 -34
  697. package/src/daemon/message-types/conversations.ts +65 -2
  698. package/src/daemon/message-types/host-transfer.ts +41 -0
  699. package/src/daemon/message-types/integrations.ts +6 -0
  700. package/src/daemon/message-types/messages.ts +26 -14
  701. package/src/daemon/message-types/schedules.ts +1 -0
  702. package/src/daemon/message-types/settings.ts +0 -6
  703. package/src/daemon/message-types/shared.ts +5 -2
  704. package/src/daemon/message-types/subagents.ts +2 -1
  705. package/src/daemon/message-types/workspace.ts +0 -2
  706. package/src/daemon/pkb-reminder-builder.test.ts +13 -12
  707. package/src/daemon/pkb-reminder-builder.ts +8 -16
  708. package/src/daemon/process-message.ts +616 -0
  709. package/src/daemon/providers-setup.ts +14 -6
  710. package/src/daemon/server.ts +79 -1272
  711. package/src/daemon/shutdown-handlers.ts +3 -13
  712. package/src/daemon/startup-error.ts +1 -1
  713. package/src/daemon/tool-side-effects.ts +14 -56
  714. package/src/daemon/trust-context.ts +32 -0
  715. package/src/daemon/wake-target-adapter.ts +223 -0
  716. package/src/email/feature-gate.ts +1 -1
  717. package/src/events/domain-events.ts +1 -8
  718. package/src/events/tool-audit-listener.ts +2 -8
  719. package/src/events/tool-metrics-listener.ts +1 -4
  720. package/src/filing/filing-service.ts +194 -54
  721. package/src/followups/followup-store.ts +3 -71
  722. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +228 -0
  723. package/src/heartbeat/heartbeat-service.ts +52 -8
  724. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  725. package/src/home/__tests__/phase5-exit-criteria.test.ts +18 -1
  726. package/src/home/__tests__/rollup-producer.test.ts +67 -2
  727. package/src/home/assistant-feed-authoring.ts +8 -1
  728. package/src/home/emit-feed-event.ts +7 -0
  729. package/src/home/feed-types.ts +42 -3
  730. package/src/home/relationship-state-writer.ts +1 -1
  731. package/src/home/rewrite-command-preview.ts +66 -0
  732. package/src/home/rewrite-feed-title.ts +58 -0
  733. package/src/home/rollup-producer.ts +16 -3
  734. package/src/inbound/platform-callback-registration.ts +1 -17
  735. package/src/ipc/__tests__/attachment-ipc.test.ts +128 -66
  736. package/src/ipc/__tests__/browser-ipc.test.ts +75 -51
  737. package/src/ipc/__tests__/cache-ipc.test.ts +52 -107
  738. package/src/ipc/__tests__/cli-ipc.test.ts +9 -6
  739. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +254 -0
  740. package/src/ipc/__tests__/skill-server.test.ts +182 -0
  741. package/src/ipc/__tests__/socket-path.test.ts +44 -37
  742. package/src/ipc/__tests__/ui-request-route.test.ts +241 -216
  743. package/src/ipc/__tests__/watcher-ipc.test.ts +33 -33
  744. package/src/ipc/assistant-server.ts +450 -0
  745. package/src/ipc/cli-client.ts +3 -3
  746. package/src/ipc/gateway-client.test.ts +131 -0
  747. package/src/ipc/gateway-client.ts +98 -120
  748. package/src/ipc/ipc-framing.ts +281 -0
  749. package/src/ipc/routes/__tests__/memory-v2-backfill.test.ts +152 -0
  750. package/src/ipc/routes/__tests__/memory-v2-validate.test.ts +219 -0
  751. package/src/ipc/routes/db-proxy.ts +73 -0
  752. package/src/ipc/routes/route-adapter.ts +32 -0
  753. package/src/ipc/routes/trust-rules.test.ts +218 -0
  754. package/src/ipc/skill-ipc-types.ts +13 -0
  755. package/src/ipc/skill-routes/__tests__/config.test.ts +146 -0
  756. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +402 -0
  757. package/src/ipc/skill-routes/__tests__/identity.test.ts +81 -0
  758. package/src/ipc/skill-routes/__tests__/log.test.ts +133 -0
  759. package/src/ipc/skill-routes/__tests__/memory.test.ts +178 -0
  760. package/src/ipc/skill-routes/__tests__/platform.test.ts +111 -0
  761. package/src/ipc/skill-routes/__tests__/providers.test.ts +265 -0
  762. package/src/ipc/skill-routes/__tests__/registries.test.ts +361 -0
  763. package/src/ipc/skill-routes/config.ts +47 -0
  764. package/src/ipc/skill-routes/events.ts +131 -0
  765. package/src/ipc/skill-routes/identity.ts +34 -0
  766. package/src/ipc/skill-routes/index.ts +37 -0
  767. package/src/ipc/skill-routes/log.ts +40 -0
  768. package/src/ipc/skill-routes/memory.ts +76 -0
  769. package/src/ipc/skill-routes/platform.ts +39 -0
  770. package/src/ipc/skill-routes/providers.ts +163 -0
  771. package/src/ipc/skill-routes/registries.ts +393 -0
  772. package/src/ipc/skill-server.ts +771 -0
  773. package/src/ipc/skill-socket-path.ts +20 -0
  774. package/src/ipc/socket-cleanup.ts +92 -0
  775. package/src/ipc/socket-path.ts +55 -48
  776. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +374 -0
  777. package/src/live-voice/__tests__/live-voice-archive.test.ts +525 -0
  778. package/src/live-voice/__tests__/live-voice-events.test.ts +473 -0
  779. package/src/live-voice/__tests__/live-voice-integration.test.ts +359 -0
  780. package/src/live-voice/__tests__/live-voice-metrics.test.ts +179 -0
  781. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +349 -0
  782. package/src/live-voice/__tests__/live-voice-stt.test.ts +244 -0
  783. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +337 -0
  784. package/src/live-voice/__tests__/live-voice-tts.test.ts +337 -0
  785. package/src/live-voice/__tests__/protocol.test.ts +295 -0
  786. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +421 -0
  787. package/src/live-voice/live-voice-archive.ts +758 -0
  788. package/src/live-voice/live-voice-metrics.ts +472 -0
  789. package/src/live-voice/live-voice-session-manager.ts +222 -0
  790. package/src/live-voice/live-voice-session.ts +1144 -0
  791. package/src/live-voice/live-voice-tts.ts +260 -0
  792. package/src/live-voice/protocol.ts +524 -0
  793. package/src/mcp/client.ts +2 -2
  794. package/src/media/app-icon-generator.ts +23 -46
  795. package/src/media/avatar-router.ts +26 -41
  796. package/src/media/gemini-image-service.ts +8 -41
  797. package/src/media/image-credentials.ts +73 -0
  798. package/src/media/image-service.ts +85 -0
  799. package/src/media/openai-image-service.ts +131 -0
  800. package/src/media/types.ts +46 -0
  801. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +4 -28
  802. package/src/memory/__tests__/auto-analysis-guard.test.ts +2 -2
  803. package/src/memory/__tests__/conversation-analyze-job.test.ts +7 -62
  804. package/src/memory/__tests__/conversation-group-migration.test.ts +2 -2
  805. package/src/memory/__tests__/find-analysis-conversation.test.ts +2 -1
  806. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +235 -0
  807. package/src/memory/admin.ts +65 -7
  808. package/src/memory/app-git-service.ts +0 -14
  809. package/src/memory/attachments-store.ts +14 -16
  810. package/src/memory/auto-analysis-enqueue.ts +2 -17
  811. package/src/memory/canonical-guardian-store.ts +2 -1
  812. package/src/memory/channel-verification-sessions.ts +1 -1
  813. package/src/memory/checkpoints.ts +1 -1
  814. package/src/memory/context-search/agent-protocol.ts +424 -0
  815. package/src/memory/context-search/agent-runner.ts +1295 -0
  816. package/src/memory/context-search/format.ts +160 -0
  817. package/src/memory/context-search/limits.ts +106 -0
  818. package/src/memory/context-search/search.ts +387 -0
  819. package/src/memory/context-search/sources/conversations.ts +278 -0
  820. package/src/memory/context-search/sources/memory.ts +90 -0
  821. package/src/memory/context-search/sources/pkb.ts +468 -0
  822. package/src/memory/context-search/sources/workspace.ts +1255 -0
  823. package/src/memory/context-search/types.ts +49 -0
  824. package/src/memory/conversation-analyze-job.ts +3 -24
  825. package/src/memory/conversation-attention-store.ts +1 -1
  826. package/src/memory/conversation-bootstrap.ts +1 -1
  827. package/src/memory/conversation-crud.ts +117 -145
  828. package/src/memory/conversation-directories.ts +1 -11
  829. package/src/memory/conversation-display-order-migration.ts +11 -2
  830. package/src/memory/conversation-group-migration.ts +20 -4
  831. package/src/memory/conversation-key-store.ts +3 -4
  832. package/src/memory/conversation-queries.ts +69 -29
  833. package/src/memory/conversation-starter-validation.ts +88 -0
  834. package/src/memory/conversation-starters-cadence.ts +1 -1
  835. package/src/memory/conversation-title-service.ts +27 -1
  836. package/src/memory/db-init.ts +22 -4
  837. package/src/memory/db-maintenance.ts +1 -1
  838. package/src/memory/delivery-channels.ts +1 -14
  839. package/src/memory/delivery-crud.ts +2 -32
  840. package/src/memory/delivery-status.ts +1 -1
  841. package/src/memory/embedding-gemini.test.ts +44 -5
  842. package/src/memory/embedding-gemini.ts +6 -1
  843. package/src/memory/external-conversation-store.ts +1 -1
  844. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +412 -0
  845. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +225 -0
  846. package/src/memory/graph/bootstrap.test.ts +277 -0
  847. package/src/memory/graph/bootstrap.ts +10 -6
  848. package/src/memory/graph/capability-seed.ts +3 -3
  849. package/src/memory/graph/compaction.ts +1 -1
  850. package/src/memory/graph/consolidation.ts +13 -10
  851. package/src/memory/graph/conversation-graph-memory.ts +151 -1
  852. package/src/memory/graph/decay.ts +1 -1
  853. package/src/memory/graph/extraction.ts +63 -23
  854. package/src/memory/graph/graph-memory-state-store.ts +1 -1
  855. package/src/memory/graph/graph-search.test.ts +95 -2
  856. package/src/memory/graph/graph-search.ts +22 -7
  857. package/src/memory/graph/image-ref-utils.ts +1 -1
  858. package/src/memory/graph/retriever.test.ts +158 -4
  859. package/src/memory/graph/retriever.ts +27 -8
  860. package/src/memory/graph/store.test.ts +2 -1
  861. package/src/memory/graph/store.ts +1 -1
  862. package/src/memory/graph/tool-handlers.ts +73 -247
  863. package/src/memory/graph/tools.ts +35 -53
  864. package/src/memory/group-crud.ts +1 -2
  865. package/src/memory/guardian-action-store.ts +2 -1
  866. package/src/memory/guardian-approvals.ts +1 -1
  867. package/src/memory/guardian-rate-limits.ts +1 -1
  868. package/src/memory/indexer.ts +43 -17
  869. package/src/memory/invite-store.ts +1 -1
  870. package/src/memory/job-handlers/backfill.ts +1 -1
  871. package/src/memory/job-handlers/cleanup.ts +2 -1
  872. package/src/memory/job-handlers/conversation-starters.ts +18 -10
  873. package/src/memory/job-handlers/embedding.test.ts +2 -1
  874. package/src/memory/job-handlers/embedding.ts +1 -1
  875. package/src/memory/job-handlers/index-maintenance.ts +1 -1
  876. package/src/memory/job-handlers/summarization.ts +3 -3
  877. package/src/memory/job-utils.ts +3 -3
  878. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +362 -0
  879. package/src/memory/jobs/embed-concept-page.ts +210 -0
  880. package/src/memory/jobs/embed-pkb-file.test.ts +2 -1
  881. package/src/memory/jobs-store.ts +10 -2
  882. package/src/memory/jobs-worker.ts +58 -5
  883. package/src/memory/lifecycle-events-store.ts +1 -1
  884. package/src/memory/llm-request-log-store.ts +1 -1
  885. package/src/memory/llm-usage-store.ts +1 -1
  886. package/src/memory/media-store.ts +1 -1
  887. package/src/memory/memory-recall-log-store.ts +1 -1
  888. package/src/memory/migrations/038-actor-token-records.ts +3 -0
  889. package/src/memory/migrations/039-actor-refresh-token-records.ts +3 -0
  890. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  891. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  892. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  893. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  894. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  895. package/src/memory/migrations/226-schedule-wake-conversation-id.ts +11 -0
  896. package/src/memory/migrations/227-add-conversation-inference-profile.ts +18 -0
  897. package/src/memory/migrations/228-rename-inference-profile-snake-case.ts +27 -0
  898. package/src/memory/migrations/229-delete-private-conversations.test.ts +1087 -0
  899. package/src/memory/migrations/229-delete-private-conversations.ts +210 -0
  900. package/src/memory/migrations/230-acp-session-history.ts +41 -0
  901. package/src/memory/migrations/231-repair-memory-graph-event-dates.ts +128 -0
  902. package/src/memory/migrations/232-activation-state.ts +38 -0
  903. package/src/memory/migrations/index.ts +14 -0
  904. package/src/memory/migrations/registry.ts +7 -0
  905. package/src/memory/pkb/pkb-index.test.ts +5 -5
  906. package/src/memory/pkb/pkb-reconcile.test.ts +5 -5
  907. package/src/memory/pkb/pkb-search.test.ts +148 -7
  908. package/src/memory/pkb/pkb-search.ts +65 -30
  909. package/src/memory/published-pages-store.ts +1 -1
  910. package/src/memory/qdrant-client.test.ts +60 -0
  911. package/src/memory/qdrant-client.ts +25 -0
  912. package/src/memory/schema/acp.ts +30 -0
  913. package/src/memory/schema/conversations.ts +1 -1
  914. package/src/memory/schema/index.ts +1 -0
  915. package/src/memory/schema/infrastructure.ts +2 -32
  916. package/src/memory/schema/memory-graph.ts +36 -14
  917. package/src/memory/schema/oauth.ts +4 -1
  918. package/src/memory/scoped-approval-grants.ts +2 -1
  919. package/src/memory/search/semantic.ts +2 -2
  920. package/src/memory/shared-app-links-store.ts +2 -1
  921. package/src/memory/tool-usage-store.ts +1 -1
  922. package/src/memory/trace-event-store.ts +2 -1
  923. package/src/memory/turn-events-store.ts +1 -1
  924. package/src/memory/v2/__tests__/activation-store.test.ts +202 -0
  925. package/src/memory/v2/__tests__/activation.test.ts +956 -0
  926. package/src/memory/v2/__tests__/backfill-jobs.test.ts +610 -0
  927. package/src/memory/v2/__tests__/consolidation-job.test.ts +395 -0
  928. package/src/memory/v2/__tests__/edges.test.ts +435 -0
  929. package/src/memory/v2/__tests__/injection.test.ts +792 -0
  930. package/src/memory/v2/__tests__/migration.test.ts +812 -0
  931. package/src/memory/v2/__tests__/page-store.test.ts +334 -0
  932. package/src/memory/v2/__tests__/qdrant.test.ts +438 -0
  933. package/src/memory/v2/__tests__/sim.test.ts +549 -0
  934. package/src/memory/v2/__tests__/skill-content.test.ts +85 -0
  935. package/src/memory/v2/__tests__/skill-qdrant.test.ts +657 -0
  936. package/src/memory/v2/__tests__/skill-store.test.ts +351 -0
  937. package/src/memory/v2/__tests__/sweep-job.test.ts +441 -0
  938. package/src/memory/v2/activation-store.ts +109 -0
  939. package/src/memory/v2/activation.ts +490 -0
  940. package/src/memory/v2/backfill-jobs.ts +442 -0
  941. package/src/memory/v2/consolidation-job.ts +304 -0
  942. package/src/memory/v2/edges.ts +217 -0
  943. package/src/memory/v2/injection.ts +307 -0
  944. package/src/memory/v2/migration.ts +654 -0
  945. package/src/memory/v2/now-text.ts +38 -0
  946. package/src/memory/v2/page-store.ts +245 -0
  947. package/src/memory/v2/prompts/consolidation.ts +185 -0
  948. package/src/memory/v2/prompts/sweep.ts +56 -0
  949. package/src/memory/v2/qdrant.ts +342 -0
  950. package/src/memory/v2/sim.ts +206 -0
  951. package/src/memory/v2/skill-content.ts +42 -0
  952. package/src/memory/v2/skill-qdrant.ts +395 -0
  953. package/src/memory/v2/skill-store.ts +128 -0
  954. package/src/memory/v2/sweep-job.ts +298 -0
  955. package/src/memory/v2/types.ts +116 -0
  956. package/src/memory/validation.ts +1 -1
  957. package/src/messaging/providers/index.ts +262 -0
  958. package/src/messaging/providers/slack/api.ts +242 -0
  959. package/src/messaging/providers/slack/message-metadata.ts +1 -1
  960. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  961. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  962. package/src/messaging/providers/slack/send.ts +383 -0
  963. package/src/messaging/providers/telegram-bot/adapter.ts +4 -42
  964. package/src/messaging/providers/telegram-bot/api.ts +253 -0
  965. package/src/messaging/providers/telegram-bot/client.ts +17 -58
  966. package/src/messaging/providers/telegram-bot/send.ts +232 -0
  967. package/src/messaging/providers/whatsapp/adapter.ts +4 -36
  968. package/src/messaging/providers/whatsapp/api.ts +319 -0
  969. package/src/messaging/providers/whatsapp/client.ts +4 -48
  970. package/src/messaging/providers/whatsapp/send.ts +209 -0
  971. package/src/notifications/adapters/slack.ts +5 -23
  972. package/src/notifications/adapters/telegram.ts +8 -29
  973. package/src/notifications/conversation-candidates.ts +1 -1
  974. package/src/notifications/conversation-pairing.ts +78 -19
  975. package/src/notifications/conversation-seed-composer.ts +12 -6
  976. package/src/notifications/copy-composer.ts +1 -6
  977. package/src/notifications/decision-engine.ts +1 -1
  978. package/src/notifications/decisions-store.ts +1 -1
  979. package/src/notifications/deliveries-store.ts +2 -1
  980. package/src/notifications/deterministic-checks.ts +1 -1
  981. package/src/notifications/emit-signal.ts +1 -1
  982. package/src/notifications/events-store.ts +1 -13
  983. package/src/notifications/preferences-store.ts +1 -1
  984. package/src/notifications/signal.ts +1 -11
  985. package/src/oauth/AGENTS.md +1 -1
  986. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  987. package/src/oauth/connect-orchestrator.ts +8 -34
  988. package/src/oauth/connect-types.ts +6 -10
  989. package/src/oauth/connection-resolver.ts +11 -2
  990. package/src/oauth/manual-token-connection.ts +23 -0
  991. package/src/oauth/oauth-store.ts +32 -15
  992. package/src/oauth/provider-serializer.ts +6 -1
  993. package/src/oauth/seed-providers.ts +56 -108
  994. package/src/outbound-proxy/http-forwarder.ts +9 -0
  995. package/src/outbound-proxy/index.ts +0 -1
  996. package/src/permissions/approval-policy.test.ts +398 -106
  997. package/src/permissions/approval-policy.ts +134 -108
  998. package/src/permissions/checker.test.ts +632 -0
  999. package/src/permissions/checker.ts +280 -345
  1000. package/src/permissions/gateway-threshold-reader.ts +177 -0
  1001. package/src/permissions/ipc-risk-types.ts +95 -0
  1002. package/src/permissions/prompter.ts +8 -9
  1003. package/src/permissions/risk-types.ts +24 -153
  1004. package/src/permissions/types.ts +19 -47
  1005. package/src/permissions/workspace-policy.ts +10 -7
  1006. package/src/playbooks/playbook-compiler.ts +1 -1
  1007. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  1008. package/src/plugins/defaults/compaction.ts +145 -0
  1009. package/src/plugins/defaults/empty-response.ts +126 -0
  1010. package/src/plugins/defaults/history-repair.ts +85 -0
  1011. package/src/plugins/defaults/index.ts +116 -0
  1012. package/src/plugins/defaults/injectors.ts +488 -0
  1013. package/src/plugins/defaults/llm-call.ts +79 -0
  1014. package/src/plugins/defaults/memory-retrieval.ts +221 -0
  1015. package/src/plugins/defaults/overflow-reduce.ts +185 -0
  1016. package/src/plugins/defaults/persistence.ts +129 -0
  1017. package/src/plugins/defaults/title-generate.ts +95 -0
  1018. package/src/plugins/defaults/token-estimate.ts +103 -0
  1019. package/src/plugins/defaults/tool-error.ts +126 -0
  1020. package/src/plugins/defaults/tool-execute.ts +89 -0
  1021. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  1022. package/src/plugins/pipeline.ts +316 -0
  1023. package/src/plugins/plugin-skill-contributions.ts +292 -0
  1024. package/src/plugins/registry.ts +301 -0
  1025. package/src/plugins/types.ts +1133 -0
  1026. package/src/plugins/user-loader.ts +203 -0
  1027. package/src/prompts/__tests__/system-prompt-memory-v2.test.ts +197 -0
  1028. package/src/prompts/persona-resolver.ts +2 -4
  1029. package/src/prompts/system-prompt.ts +39 -0
  1030. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  1031. package/src/prompts/templates/SOUL.md +3 -1
  1032. package/src/providers/__tests__/provider-env-vars.test.ts +0 -21
  1033. package/src/providers/__tests__/retry-callsite.test.ts +3 -6
  1034. package/src/providers/anthropic/client.ts +71 -19
  1035. package/src/providers/call-site-routing.ts +7 -3
  1036. package/src/providers/fireworks/client.ts +3 -0
  1037. package/src/providers/gemini/client.ts +96 -22
  1038. package/src/providers/managed-proxy/context.ts +0 -12
  1039. package/src/providers/model-catalog.ts +123 -25
  1040. package/src/providers/model-intents.ts +6 -7
  1041. package/src/providers/openai/chat-completions-provider.ts +37 -7
  1042. package/src/providers/openai/responses-provider.ts +39 -4
  1043. package/src/providers/openrouter/client.ts +9 -6
  1044. package/src/providers/provider-env-vars.ts +4 -12
  1045. package/src/providers/provider-send-message.ts +16 -11
  1046. package/src/providers/registry.ts +1 -1
  1047. package/src/providers/retry.ts +52 -23
  1048. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  1049. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  1050. package/src/providers/speech-to-text/openai-whisper-stream.ts +1 -1
  1051. package/src/providers/speech-to-text/openai-whisper.ts +3 -6
  1052. package/src/providers/speech-to-text/provider-catalog.ts +75 -0
  1053. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  1054. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  1055. package/src/providers/speech-to-text/xai.ts +5 -5
  1056. package/src/providers/thinking-config.ts +34 -0
  1057. package/src/providers/types.ts +22 -10
  1058. package/src/runtime/AGENTS.md +27 -17
  1059. package/src/runtime/__tests__/agent-wake.test.ts +33 -9
  1060. package/src/runtime/__tests__/client-registry.test.ts +271 -0
  1061. package/src/runtime/__tests__/interactive-ui.test.ts +157 -246
  1062. package/src/runtime/access-request-helper.ts +9 -20
  1063. package/src/runtime/actor-trust-resolver.ts +2 -2
  1064. package/src/runtime/agent-wake.ts +174 -68
  1065. package/src/runtime/approval-conversation-turn.ts +2 -15
  1066. package/src/runtime/approval-message-composer.ts +11 -60
  1067. package/src/runtime/assistant-event.ts +18 -66
  1068. package/src/runtime/auth/__tests__/guard-tests.test.ts +6 -30
  1069. package/src/runtime/auth/__tests__/middleware.test.ts +10 -10
  1070. package/src/runtime/auth/__tests__/route-policy.test.ts +0 -8
  1071. package/src/runtime/auth/context.ts +9 -0
  1072. package/src/runtime/auth/middleware.ts +4 -4
  1073. package/src/runtime/auth/route-policy.ts +195 -4
  1074. package/src/runtime/auth/token-service.ts +1 -100
  1075. package/src/runtime/capability-tokens.ts +89 -313
  1076. package/src/runtime/channel-approval-types.ts +1 -6
  1077. package/src/runtime/channel-approvals.ts +7 -79
  1078. package/src/runtime/channel-readiness-service.ts +2 -2
  1079. package/src/runtime/channel-reply-delivery.ts +2 -8
  1080. package/src/runtime/channel-retry-sweep.ts +20 -17
  1081. package/src/runtime/client-registry.ts +254 -0
  1082. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -7
  1083. package/src/runtime/gateway-client.ts +37 -378
  1084. package/src/runtime/guardian-action-grant-minter.ts +2 -3
  1085. package/src/runtime/guardian-action-message-composer.ts +11 -52
  1086. package/src/runtime/guardian-action-service.ts +19 -7
  1087. package/src/runtime/guardian-decision-types.ts +4 -65
  1088. package/src/runtime/guardian-reply-router.ts +10 -19
  1089. package/src/runtime/guardian-vellum-migration.ts +5 -64
  1090. package/src/runtime/http-errors.ts +3 -0
  1091. package/src/runtime/http-router.ts +50 -7
  1092. package/src/runtime/http-server.ts +345 -1041
  1093. package/src/runtime/http-types.ts +15 -100
  1094. package/src/runtime/interactive-ui-types.ts +145 -0
  1095. package/src/runtime/interactive-ui.ts +38 -196
  1096. package/src/runtime/invite-redemption-service.ts +1 -1
  1097. package/src/runtime/invite-redemption-templates.ts +1 -1
  1098. package/src/runtime/local-actor-identity.ts +13 -43
  1099. package/src/runtime/message-composer-types.ts +134 -0
  1100. package/src/runtime/middleware/rate-limiter.ts +1 -1
  1101. package/src/runtime/middleware/request-logger.ts +5 -2
  1102. package/src/runtime/migrations/__tests__/job-registry.test.ts +346 -0
  1103. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +16 -0
  1104. package/src/runtime/migrations/job-registry.ts +281 -0
  1105. package/src/runtime/migrations/vbundle-builder.ts +4 -26
  1106. package/src/runtime/migrations/vbundle-importer.ts +1 -1
  1107. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -13
  1108. package/src/runtime/migrations/vbundle-tar-stream.ts +11 -3
  1109. package/src/runtime/nl-approval-parser.ts +16 -21
  1110. package/src/runtime/pending-interactions.ts +29 -12
  1111. package/src/runtime/routes/__tests__/acp-routes.test.ts +395 -0
  1112. package/src/runtime/routes/__tests__/backup-routes.test.ts +204 -320
  1113. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +72 -4
  1114. package/src/runtime/routes/__tests__/stt-routes.test.ts +182 -223
  1115. package/src/runtime/routes/__tests__/suggest-trust-rule-routes.test.ts +230 -0
  1116. package/src/{ipc/__tests__/task-ipc.test.ts → runtime/routes/__tests__/task-routes.test.ts} +116 -96
  1117. package/src/runtime/routes/__tests__/tts-routes.test.ts +185 -289
  1118. package/src/runtime/routes/access-request-decision.ts +25 -50
  1119. package/src/runtime/routes/acp-routes.test.ts +371 -0
  1120. package/src/runtime/routes/acp-routes.ts +392 -166
  1121. package/src/runtime/routes/app-management-routes.ts +464 -660
  1122. package/src/runtime/routes/app-routes.ts +192 -177
  1123. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  1124. package/src/runtime/routes/approval-routes.ts +133 -434
  1125. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +24 -84
  1126. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +3 -10
  1127. package/src/runtime/routes/attachment-routes.ts +409 -253
  1128. package/src/runtime/routes/audio-routes.ts +51 -18
  1129. package/src/runtime/routes/avatar-routes.ts +82 -75
  1130. package/src/runtime/routes/background-tool-routes.ts +94 -0
  1131. package/src/runtime/routes/backup-routes.ts +154 -336
  1132. package/src/runtime/routes/brain-graph-routes.ts +83 -110
  1133. package/src/runtime/routes/browser-routes.ts +141 -0
  1134. package/src/runtime/routes/btw-routes.ts +62 -106
  1135. package/src/runtime/routes/cache-routes.ts +96 -0
  1136. package/src/runtime/routes/call-routes.ts +208 -247
  1137. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +1 -1
  1138. package/src/runtime/routes/channel-delivery-routes.ts +25 -27
  1139. package/src/runtime/routes/channel-readiness-routes.ts +83 -120
  1140. package/src/runtime/routes/channel-route-definitions.ts +62 -0
  1141. package/src/runtime/routes/channel-route-shared.ts +14 -18
  1142. package/src/runtime/routes/channel-verification-routes.ts +207 -187
  1143. package/src/runtime/routes/client-routes.ts +48 -0
  1144. package/src/runtime/routes/contact-routes.ts +533 -407
  1145. package/src/runtime/routes/conversation-analysis-routes.ts +48 -49
  1146. package/src/runtime/routes/conversation-attention-routes.ts +55 -67
  1147. package/src/runtime/routes/conversation-list-routes.ts +265 -0
  1148. package/src/runtime/routes/conversation-management-routes.ts +626 -715
  1149. package/src/runtime/routes/conversation-query-routes.ts +510 -460
  1150. package/src/runtime/routes/conversation-routes.ts +652 -457
  1151. package/src/runtime/routes/conversation-starter-routes.ts +121 -71
  1152. package/src/runtime/routes/credential-prompt-routes.ts +124 -0
  1153. package/src/runtime/routes/debug-routes.ts +34 -39
  1154. package/src/runtime/routes/defer-routes.ts +230 -0
  1155. package/src/runtime/routes/diagnostics-routes.ts +79 -70
  1156. package/src/runtime/routes/documents-routes.ts +117 -106
  1157. package/src/runtime/routes/errors.ts +132 -0
  1158. package/src/runtime/routes/events-routes.ts +97 -58
  1159. package/src/runtime/routes/filing-routes.ts +65 -78
  1160. package/src/runtime/routes/global-search-routes.ts +51 -57
  1161. package/src/runtime/routes/group-routes.ts +199 -181
  1162. package/src/runtime/routes/guardian-action-routes.ts +103 -169
  1163. package/src/runtime/routes/guardian-approval-interception.ts +27 -58
  1164. package/src/runtime/routes/guardian-approval-prompt.ts +10 -21
  1165. package/src/runtime/routes/guardian-approval-reply-helpers.ts +2 -6
  1166. package/src/runtime/routes/guardian-expiry-sweep.ts +19 -36
  1167. package/src/runtime/routes/heartbeat-routes.ts +194 -209
  1168. package/src/runtime/routes/home-feed-routes.ts +85 -187
  1169. package/src/runtime/routes/home-state-routes.ts +27 -24
  1170. package/src/runtime/routes/host-bash-routes.ts +42 -52
  1171. package/src/runtime/routes/host-browser-routes.ts +38 -69
  1172. package/src/runtime/routes/host-cu-routes.ts +74 -70
  1173. package/src/runtime/routes/host-file-routes.ts +50 -60
  1174. package/src/runtime/routes/host-transfer-routes.ts +220 -0
  1175. package/src/runtime/routes/http-adapter.ts +172 -0
  1176. package/src/runtime/routes/identity-routes.ts +83 -79
  1177. package/src/runtime/routes/inbound-conversation.ts +11 -18
  1178. package/src/runtime/routes/inbound-message-handler.ts +162 -123
  1179. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +79 -138
  1180. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +2 -3
  1181. package/src/runtime/routes/inbound-stages/background-dispatch.ts +54 -90
  1182. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +25 -50
  1183. package/src/runtime/routes/inbound-stages/edit-intercept.ts +7 -7
  1184. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  1185. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +5 -6
  1186. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -24
  1187. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -10
  1188. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -4
  1189. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +3 -3
  1190. package/src/runtime/routes/inbound-stages/verification-intercept.ts +19 -26
  1191. package/src/runtime/routes/index.ts +197 -0
  1192. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +25 -32
  1193. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +22 -31
  1194. package/src/runtime/routes/integrations/slack/channel.ts +69 -66
  1195. package/src/runtime/routes/integrations/slack/share.ts +49 -58
  1196. package/src/runtime/routes/integrations/telegram.ts +91 -74
  1197. package/src/runtime/routes/integrations/twilio.ts +163 -240
  1198. package/src/runtime/routes/integrations/vercel.ts +57 -54
  1199. package/src/runtime/routes/interface-routes.ts +43 -0
  1200. package/src/runtime/routes/internal-oauth-routes.ts +56 -0
  1201. package/src/runtime/routes/internal-twilio-routes.ts +46 -0
  1202. package/src/runtime/routes/llm-context-normalization.ts +4 -2
  1203. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  1204. package/src/runtime/routes/log-export-routes.ts +90 -100
  1205. package/src/runtime/routes/memory-item-routes.test.ts +153 -175
  1206. package/src/runtime/routes/memory-item-routes.ts +243 -323
  1207. package/src/runtime/routes/memory-v2-routes.ts +193 -0
  1208. package/src/runtime/routes/migration-rollback-routes.ts +167 -212
  1209. package/src/runtime/routes/migration-routes.ts +877 -377
  1210. package/src/runtime/routes/notification-routes.ts +199 -70
  1211. package/src/runtime/routes/oauth-apps.ts +254 -251
  1212. package/src/runtime/routes/oauth-providers.ts +66 -57
  1213. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +224 -0
  1214. package/src/runtime/routes/playground/__tests__/guard.test.ts +60 -0
  1215. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +250 -0
  1216. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +195 -0
  1217. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +159 -0
  1218. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +207 -0
  1219. package/src/runtime/routes/playground/__tests__/state.test.ts +175 -0
  1220. package/src/runtime/routes/playground/conversation-not-found.ts +27 -0
  1221. package/src/runtime/routes/playground/force-compact.ts +60 -0
  1222. package/src/runtime/routes/playground/guard.ts +36 -0
  1223. package/src/runtime/routes/playground/helpers.ts +103 -0
  1224. package/src/runtime/routes/playground/index.ts +18 -0
  1225. package/src/runtime/routes/playground/inject-failures.ts +143 -0
  1226. package/src/runtime/routes/playground/reset-circuit.ts +89 -0
  1227. package/src/runtime/routes/playground/seed-conversation.ts +113 -0
  1228. package/src/runtime/routes/playground/seeded-conversations.ts +74 -0
  1229. package/src/runtime/routes/playground/state.ts +77 -0
  1230. package/src/runtime/routes/profiler-routes.ts +132 -167
  1231. package/src/runtime/routes/ps-routes.ts +120 -0
  1232. package/src/runtime/routes/recording-routes.ts +197 -258
  1233. package/src/runtime/routes/rename-conversation-routes.ts +89 -0
  1234. package/src/runtime/routes/schedule-routes.ts +284 -207
  1235. package/src/runtime/routes/secret-routes.ts +219 -265
  1236. package/src/runtime/routes/secrets-deps.ts +24 -0
  1237. package/src/runtime/routes/settings-routes.ts +361 -441
  1238. package/src/runtime/routes/skills-routes.ts +434 -469
  1239. package/src/runtime/routes/stt-routes.ts +196 -206
  1240. package/src/runtime/routes/subagents-routes.ts +125 -141
  1241. package/src/runtime/routes/suggest-trust-rule-routes.ts +244 -0
  1242. package/src/runtime/routes/surface-action-routes.ts +135 -190
  1243. package/src/runtime/routes/surface-content-routes.ts +84 -118
  1244. package/src/runtime/routes/task-routes.ts +354 -0
  1245. package/src/runtime/routes/telemetry-routes.ts +33 -49
  1246. package/src/runtime/routes/trace-event-routes.ts +55 -74
  1247. package/src/runtime/routes/trust-rules-routes.ts +147 -239
  1248. package/src/runtime/routes/tts-routes.ts +187 -169
  1249. package/src/runtime/routes/types.ts +139 -0
  1250. package/src/{ipc/routes/ui-request.ts → runtime/routes/ui-request-routes.ts} +23 -17
  1251. package/src/runtime/routes/upgrade-broadcast-routes.ts +156 -197
  1252. package/src/runtime/routes/usage-routes.ts +143 -169
  1253. package/src/runtime/routes/user-routes.ts +102 -18
  1254. package/src/runtime/routes/wake-conversation-routes.ts +49 -0
  1255. package/src/{ipc/routes/watcher.ts → runtime/routes/watcher-routes.ts} +84 -39
  1256. package/src/runtime/routes/wipe-conversation-routes.ts +89 -0
  1257. package/src/runtime/routes/work-items-routes.test.ts +10 -20
  1258. package/src/runtime/routes/work-items-routes.ts +418 -433
  1259. package/src/runtime/routes/workspace-commit-routes.ts +30 -61
  1260. package/src/runtime/routes/workspace-routes.test.ts +254 -381
  1261. package/src/runtime/routes/workspace-routes.ts +238 -246
  1262. package/src/runtime/runtime-mode.ts +8 -1
  1263. package/src/runtime/services/__tests__/analyze-conversation.test.ts +80 -118
  1264. package/src/runtime/services/analyze-conversation.ts +14 -41
  1265. package/src/runtime/services/conversation-serializer.ts +181 -0
  1266. package/src/runtime/skill-route-registry.ts +75 -15
  1267. package/src/runtime/trust-context-resolver.ts +3 -2
  1268. package/src/runtime/verification-outbound-actions.ts +13 -49
  1269. package/src/schedule/run-script.ts +68 -0
  1270. package/src/schedule/schedule-store.ts +70 -2
  1271. package/src/schedule/scheduler.ts +149 -8
  1272. package/src/security/ces-credential-client.ts +32 -169
  1273. package/src/security/ces-rpc-credential-backend.ts +1 -1
  1274. package/src/security/credential-backend.ts +6 -6
  1275. package/src/security/oauth-completion-page.ts +1 -1
  1276. package/src/security/oauth2.ts +3 -6
  1277. package/src/sequence/analytics.ts +1 -1
  1278. package/src/sequence/guardrails.ts +3 -3
  1279. package/src/sequence/store.ts +2 -1
  1280. package/src/signals/bash.ts +1 -1
  1281. package/src/signals/event-stream.ts +1 -1
  1282. package/src/skills/catalog-cache.ts +19 -5
  1283. package/src/skills/catalog-files.ts +0 -5
  1284. package/src/skills/catalog-install.ts +28 -18
  1285. package/src/skills/category-inference.ts +0 -11
  1286. package/src/skills/clawhub.ts +2 -2
  1287. package/src/skills/managed-store.ts +2 -2
  1288. package/src/skills/remote-skill-policy.ts +6 -7
  1289. package/src/subagent/index.ts +2 -6
  1290. package/src/subagent/manager.ts +27 -23
  1291. package/src/subagent/types.ts +9 -0
  1292. package/src/tasks/SPEC.md +2 -2
  1293. package/src/tasks/task-compiler.ts +1 -1
  1294. package/src/tasks/task-runner.ts +2 -22
  1295. package/src/tasks/task-store.ts +1 -1
  1296. package/src/tools/acp/list-agents.test.ts +115 -0
  1297. package/src/tools/acp/list-agents.ts +31 -0
  1298. package/src/tools/acp/spawn.test.ts +379 -0
  1299. package/src/tools/acp/spawn.ts +142 -62
  1300. package/src/tools/acp/steer.test.ts +101 -0
  1301. package/src/tools/acp/steer.ts +38 -0
  1302. package/src/tools/background-tool-registry.ts +98 -0
  1303. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  1304. package/src/tools/browser/browser-execution.ts +122 -26
  1305. package/src/tools/browser/browser-manager.ts +1 -8
  1306. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  1307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  1308. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +1 -1
  1309. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +3 -1
  1310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  1311. package/src/tools/browser/cdp-client/factory.ts +15 -4
  1312. package/src/tools/browser/cdp-client/types.ts +4 -1
  1313. package/src/tools/computer-use/definitions.ts +1 -1
  1314. package/src/tools/credential-execution/make-authenticated-request.ts +2 -2
  1315. package/src/tools/credential-execution/manage-secure-command-tool.ts +1 -1
  1316. package/src/tools/credential-execution/run-authenticated-command.ts +2 -2
  1317. package/src/tools/credentials/broker-types.ts +2 -1
  1318. package/src/tools/document/editor-template.ts +1 -1
  1319. package/src/tools/execution-timeout.ts +1 -1
  1320. package/src/tools/executor.ts +123 -76
  1321. package/src/tools/host-filesystem/transfer.test.ts +268 -0
  1322. package/src/tools/host-filesystem/transfer.ts +234 -0
  1323. package/src/tools/host-terminal/host-shell.ts +189 -11
  1324. package/src/tools/mcp/mcp-tool-factory.ts +1 -1
  1325. package/src/tools/memory/register.test.ts +161 -1
  1326. package/src/tools/memory/register.ts +19 -34
  1327. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  1328. package/src/tools/permission-checker.ts +103 -255
  1329. package/src/tools/policy-context.ts +5 -8
  1330. package/src/tools/registry.ts +156 -4
  1331. package/src/tools/schedule/create.ts +23 -8
  1332. package/src/tools/schedule/update.ts +3 -1
  1333. package/src/tools/secret-detection-handler.ts +13 -154
  1334. package/src/tools/shared/shell-output.ts +4 -1
  1335. package/src/tools/side-effects.ts +2 -2
  1336. package/src/tools/skills/execute.ts +1 -1
  1337. package/src/tools/subagent/spawn.ts +35 -11
  1338. package/src/tools/system/avatar-generator.ts +6 -2
  1339. package/src/tools/terminal/safe-env.ts +9 -1
  1340. package/src/tools/terminal/shell.ts +161 -31
  1341. package/src/tools/tool-approval-handler.ts +4 -70
  1342. package/src/tools/tool-input-summary.ts +10 -0
  1343. package/src/tools/types.ts +157 -151
  1344. package/src/tools/ui-surface/definitions.ts +2 -2
  1345. package/src/util/debounce.ts +0 -21
  1346. package/src/util/errors.ts +0 -8
  1347. package/src/util/log-redact.ts +0 -1
  1348. package/src/util/platform.ts +85 -119
  1349. package/src/util/pricing.ts +135 -9
  1350. package/src/watcher/engine.ts +42 -20
  1351. package/src/watcher/watcher-store.ts +2 -1
  1352. package/src/work-items/work-item-store.ts +1 -1
  1353. package/src/workspace/git-service.ts +1 -6
  1354. package/src/workspace/migrations/006-services-config.ts +11 -4
  1355. package/src/workspace/migrations/017-seed-persona-dirs.ts +1 -1
  1356. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +1 -1
  1357. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  1358. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +1 -1
  1359. package/src/workspace/migrations/031-drop-user-md.ts +1 -1
  1360. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  1361. package/src/workspace/migrations/045-release-notes-meet-avatar.ts +3 -4
  1362. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  1363. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  1364. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  1365. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  1366. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  1367. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  1368. package/src/workspace/migrations/052-seed-default-inference-profiles.ts +150 -0
  1369. package/src/workspace/migrations/053-release-notes-acp-codex.ts +107 -0
  1370. package/src/workspace/migrations/054-seed-recall-callsite.ts +102 -0
  1371. package/src/workspace/migrations/055-release-notes-agentic-recall.ts +63 -0
  1372. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +65 -0
  1373. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +98 -0
  1374. package/src/workspace/migrations/058-release-notes-acp-sessions-ui.ts +71 -0
  1375. package/src/workspace/migrations/059-move-pid-to-workspace.ts +53 -0
  1376. package/src/workspace/migrations/060-memory-v2-init.ts +53 -0
  1377. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +1 -1
  1378. package/src/workspace/migrations/registry.ts +30 -0
  1379. package/src/workspace/migrations/runner.ts +2 -2
  1380. package/src/workspace/provider-commit-message-generator.ts +1 -1
  1381. package/tsconfig.json +1 -1
  1382. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  1383. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  1384. package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +0 -471
  1385. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +0 -436
  1386. package/src/__tests__/cli-command-risk-guard.test.ts +0 -368
  1387. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  1388. package/src/__tests__/config-watcher-feature-flags.test.ts +0 -211
  1389. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  1390. package/src/__tests__/conversation-approval-overrides.test.ts +0 -207
  1391. package/src/__tests__/conversation-host-access-routes.test.ts +0 -229
  1392. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +0 -226
  1393. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +0 -167
  1394. package/src/__tests__/ephemeral-permissions.test.ts +0 -474
  1395. package/src/__tests__/extension-id-sync-guard.test.ts +0 -241
  1396. package/src/__tests__/hooks-blocking.test.ts +0 -178
  1397. package/src/__tests__/hooks-cli.test.ts +0 -182
  1398. package/src/__tests__/hooks-config.test.ts +0 -108
  1399. package/src/__tests__/hooks-discovery.test.ts +0 -211
  1400. package/src/__tests__/hooks-integration.test.ts +0 -196
  1401. package/src/__tests__/hooks-manager.test.ts +0 -226
  1402. package/src/__tests__/hooks-runner.test.ts +0 -175
  1403. package/src/__tests__/hooks-settings.test.ts +0 -160
  1404. package/src/__tests__/hooks-templates.test.ts +0 -169
  1405. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  1406. package/src/__tests__/hooks-watch.test.ts +0 -112
  1407. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +0 -374
  1408. package/src/__tests__/native-host-marker-sync-guard.test.ts +0 -157
  1409. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  1410. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  1411. package/src/__tests__/pairing-concurrent.test.ts +0 -84
  1412. package/src/__tests__/pairing-routes.test.ts +0 -181
  1413. package/src/__tests__/parser.test.ts +0 -595
  1414. package/src/__tests__/permission-checker-host-gate.test.ts +0 -512
  1415. package/src/__tests__/permission-controls-v2-flag.test.ts +0 -55
  1416. package/src/__tests__/permission-mode.test.ts +0 -89
  1417. package/src/__tests__/provider-env-vars-scope.test.ts +0 -52
  1418. package/src/__tests__/risk-classifier-parity.test.ts +0 -230
  1419. package/src/__tests__/send-notification-tool.test.ts +0 -83
  1420. package/src/__tests__/shell-identity.test.ts +0 -370
  1421. package/src/__tests__/shell-parser-fuzz.test.ts +0 -629
  1422. package/src/__tests__/shell-parser-property.test.ts +0 -936
  1423. package/src/__tests__/starter-bundle.test.ts +0 -173
  1424. package/src/__tests__/stt-catalog-parity.test.ts +0 -282
  1425. package/src/__tests__/task-runner.test.ts +0 -224
  1426. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -357
  1427. package/src/__tests__/trust-store-pattern-matches.test.ts +0 -29
  1428. package/src/__tests__/trust-store.test.ts +0 -2013
  1429. package/src/__tests__/v2-consent-policy.test.ts +0 -103
  1430. package/src/browser/identifiers.ts +0 -51
  1431. package/src/cli/commands/shotgun.ts +0 -266
  1432. package/src/cli/db.ts +0 -1
  1433. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  1434. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  1435. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  1436. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  1437. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  1438. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  1439. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  1440. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  1441. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  1442. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  1443. package/src/config/bundled-skills/settings/tools/avatar-get.ts +0 -40
  1444. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +0 -64
  1445. package/src/config/bundled-skills/settings/tools/avatar-update.ts +0 -88
  1446. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  1447. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +0 -127
  1448. package/src/daemon/approved-devices-store.ts +0 -110
  1449. package/src/daemon/context-overflow-approval.ts +0 -52
  1450. package/src/daemon/external-skills-bootstrap.ts +0 -41
  1451. package/src/daemon/message-types/trust.ts +0 -71
  1452. package/src/daemon/pairing-store.ts +0 -229
  1453. package/src/daemon/watch-handler.ts +0 -399
  1454. package/src/hooks/cli.ts +0 -253
  1455. package/src/hooks/config.ts +0 -100
  1456. package/src/hooks/discovery.ts +0 -135
  1457. package/src/hooks/manager.ts +0 -179
  1458. package/src/hooks/runner.ts +0 -117
  1459. package/src/hooks/templates.ts +0 -77
  1460. package/src/hooks/types.ts +0 -75
  1461. package/src/ipc/cli-server.ts +0 -252
  1462. package/src/ipc/routes/attachment.ts +0 -114
  1463. package/src/ipc/routes/browser-context.ts +0 -61
  1464. package/src/ipc/routes/browser.ts +0 -96
  1465. package/src/ipc/routes/cache.ts +0 -96
  1466. package/src/ipc/routes/index.ts +0 -21
  1467. package/src/ipc/routes/task-queue.ts +0 -226
  1468. package/src/ipc/routes/task.ts +0 -173
  1469. package/src/ipc/routes/wake-conversation.ts +0 -19
  1470. package/src/memory/db.ts +0 -23
  1471. package/src/oauth/scope-policy.ts +0 -89
  1472. package/src/permissions/bash-risk-classifier.test.ts +0 -1208
  1473. package/src/permissions/bash-risk-classifier.ts +0 -707
  1474. package/src/permissions/command-registry.test.ts +0 -535
  1475. package/src/permissions/command-registry.ts +0 -825
  1476. package/src/permissions/defaults.ts +0 -313
  1477. package/src/permissions/file-risk-classifier.test.ts +0 -535
  1478. package/src/permissions/file-risk-classifier.ts +0 -274
  1479. package/src/permissions/permission-mode.ts +0 -24
  1480. package/src/permissions/shell-identity.ts +0 -337
  1481. package/src/permissions/skill-risk-classifier.test.ts +0 -311
  1482. package/src/permissions/skill-risk-classifier.ts +0 -214
  1483. package/src/permissions/trust-client.ts +0 -359
  1484. package/src/permissions/trust-store-interface.ts +0 -100
  1485. package/src/permissions/trust-store.ts +0 -1330
  1486. package/src/permissions/v2-consent-policy.ts +0 -87
  1487. package/src/permissions/web-risk-classifier.test.ts +0 -170
  1488. package/src/permissions/web-risk-classifier.ts +0 -89
  1489. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +0 -715
  1490. package/src/runtime/__tests__/capability-tokens.test.ts +0 -258
  1491. package/src/runtime/actor-refresh-token-store.ts +0 -156
  1492. package/src/runtime/actor-token-store.ts +0 -207
  1493. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -264
  1494. package/src/runtime/auth/credential-service.ts +0 -352
  1495. package/src/runtime/conversation-approval-overrides.ts +0 -86
  1496. package/src/runtime/gateway-internal-client.ts +0 -94
  1497. package/src/runtime/routes/browser-extension-pair-routes.ts +0 -556
  1498. package/src/runtime/routes/channel-routes.ts +0 -112
  1499. package/src/runtime/routes/contact-routes.test.ts +0 -298
  1500. package/src/runtime/routes/guardian-bootstrap-routes.ts +0 -175
  1501. package/src/runtime/routes/guardian-refresh-routes.ts +0 -79
  1502. package/src/runtime/routes/invite-routes.ts +0 -280
  1503. package/src/runtime/routes/pairing-routes.ts +0 -431
  1504. package/src/runtime/routes/watch-routes.ts +0 -156
  1505. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +0 -67
  1506. package/src/runtime/services/analyze-deps-singleton.ts +0 -32
  1507. package/src/signals/shotgun.ts +0 -203
  1508. package/src/tasks/ephemeral-permissions.ts +0 -55
  1509. package/src/tools/terminal/parser.ts +0 -623
  1510. package/src/tools/watch/screen-watch.ts +0 -144
  1511. package/src/tools/watch/watch-state.ts +0 -142
  1512. package/src/types/qrcode.d.ts +0 -13
  1513. package/src/util/network-info.ts +0 -55
  1514. /package/node_modules/@vellumai/{ces-contracts → ces-client}/tsconfig.json +0 -0
  1515. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/__tests__/grants.test.ts +0 -0
  1516. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/error.ts +0 -0
  1517. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/grants.ts +0 -0
  1518. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/handles.ts +0 -0
  1519. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/rendering.ts +0 -0
  1520. /package/node_modules/@vellumai/{ces-contracts → service-contracts}/src/rpc.ts +0 -0
@@ -37,8 +37,9 @@ import {
37
37
  } from "../context/token-estimator.js";
38
38
  import type { ContextWindowManager } from "../context/window-manager.js";
39
39
  import type { ToolProfiler } from "../events/tool-profiling-listener.js";
40
+ import { emitFeedEvent } from "../home/emit-feed-event.js";
40
41
  import { writeRelationshipState } from "../home/relationship-state-writer.js";
41
- import { getHookManager } from "../hooks/manager.js";
42
+ import { rewriteFeedTitle } from "../home/rewrite-feed-title.js";
42
43
  import {
43
44
  clearSentryConversationContext,
44
45
  setSentryConversationContext,
@@ -47,32 +48,63 @@ import { commitAppTurnChanges } from "../memory/app-git-service.js";
47
48
  import { getApp, listAppFiles, resolveAppDir } from "../memory/app-store.js";
48
49
  import { enqueueAutoAnalysisOnCompaction } from "../memory/auto-analysis-enqueue.js";
49
50
  import {
50
- addMessage,
51
- clearPkbSystemReminderMetadataForConversation,
52
- deleteMessageById,
51
+ clearStrippedInjectionMetadataForConversation,
53
52
  getConversation,
54
53
  getConversationOriginChannel,
55
54
  getConversationOriginInterface,
55
+ getConversationOverrideProfileFromRow,
56
56
  getLastUserTimestampBefore,
57
57
  getMessageById,
58
58
  provenanceFromTrustContext,
59
59
  updateConversationContextWindow,
60
- updateConversationTitle,
61
- updateMessageMetadata,
62
60
  } from "../memory/conversation-crud.js";
63
61
  import { getResolvedConversationDirPath } from "../memory/conversation-directories.js";
64
62
  import { syncMessageToDisk } from "../memory/conversation-disk-view.js";
65
63
  import {
66
64
  isReplaceableTitle,
67
- queueGenerateConversationTitle,
68
65
  queueRegenerateConversationTitle,
69
- UNTITLED_FALLBACK,
70
66
  } from "../memory/conversation-title-service.js";
71
67
  import type { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
72
68
  import { recordMemoryRecallLog } from "../memory/memory-recall-log-store.js";
73
69
  import { PKB_WORKSPACE_SCOPE } from "../memory/pkb/types.js";
70
+ import type { QdrantSparseVector } from "../memory/qdrant-client.js";
74
71
  import type { PermissionPrompter } from "../permissions/prompter.js";
75
- import type { ContentBlock, Message } from "../providers/types.js";
72
+ import { defaultCompactionTerminal } from "../plugins/defaults/compaction.js";
73
+ import { defaultHistoryRepairTerminal } from "../plugins/defaults/history-repair.js";
74
+ import {
75
+ asDefaultGraphPayload,
76
+ type DefaultMemoryRetrievalDeps,
77
+ type GraphMemoryPayload,
78
+ runDefaultMemoryRetrieval,
79
+ } from "../plugins/defaults/memory-retrieval.js";
80
+ import { defaultPersistenceTerminal } from "../plugins/defaults/persistence.js";
81
+ import { defaultTitleGenerateTerminal } from "../plugins/defaults/title-generate.js";
82
+ import { defaultTokenEstimateTerminal } from "../plugins/defaults/token-estimate.js";
83
+ import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
84
+ import { getMiddlewaresFor } from "../plugins/registry.js";
85
+ import type {
86
+ CircuitBreakerArgs,
87
+ CircuitBreakerResult,
88
+ CompactionArgs,
89
+ CompactionResult,
90
+ EstimateArgs,
91
+ EstimateResult,
92
+ HistoryRepairArgs,
93
+ HistoryRepairResult,
94
+ MemoryArgs,
95
+ MemoryResult,
96
+ OverflowReduceArgs,
97
+ OverflowReduceResult,
98
+ PersistArgs,
99
+ PersistResult,
100
+ TurnContext as PluginTurnContext,
101
+ } from "../plugins/types.js";
102
+ import { PluginExecutionError, PluginTimeoutError } from "../plugins/types.js";
103
+ import type {
104
+ ContentBlock,
105
+ Message,
106
+ ToolDefinition,
107
+ } from "../providers/types.js";
76
108
  import type { Provider } from "../providers/types.js";
77
109
  import { resolveActorTrust } from "../runtime/actor-trust-resolver.js";
78
110
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
@@ -88,7 +120,6 @@ import {
88
120
  type AssistantAttachmentDraft,
89
121
  cleanAssistantContent,
90
122
  } from "./assistant-attachments.js";
91
- import { requestCompressionApproval } from "./context-overflow-approval.js";
92
123
  import { resolveOverflowAction } from "./context-overflow-policy.js";
93
124
  import {
94
125
  createInitialReducerState,
@@ -117,7 +148,6 @@ import type {
117
148
  ChannelCapabilities,
118
149
  InboundActorContext,
119
150
  InjectionMode,
120
- TrustContext,
121
151
  } from "./conversation-runtime-assembly.js";
122
152
  import {
123
153
  applyRuntimeInjections,
@@ -129,8 +159,6 @@ import {
129
159
  inboundActorContextFromTrustContext,
130
160
  loadSlackActiveThreadFocusBlock,
131
161
  loadSlackChronologicalMessages,
132
- readNowScratchpad,
133
- readPkbContext,
134
162
  stripInjectionsForCompaction,
135
163
  } from "./conversation-runtime-assembly.js";
136
164
  import type { SkillProjectionCache } from "./conversation-skill-tools.js";
@@ -138,7 +166,7 @@ import { markSurfaceCompleted } from "./conversation-surfaces.js";
138
166
  import { resolveTrustClass } from "./conversation-tool-setup.js";
139
167
  import { recordUsage } from "./conversation-usage.js";
140
168
  import { formatTurnTimestamp } from "./date-context.js";
141
- import { deepRepairHistory, repairHistory } from "./history-repair.js";
169
+ import { deepRepairHistory } from "./history-repair.js";
142
170
  import type {
143
171
  DynamicPageSurfaceData,
144
172
  ServerMessage,
@@ -147,8 +175,10 @@ import type {
147
175
  UsageStats,
148
176
  } from "./message-protocol.js";
149
177
  import type { MemoryRecalled } from "./message-types/memory.js";
178
+ import type { ConfirmationStateChanged } from "./message-types/messages.js";
150
179
  import { parseActualTokensFromError } from "./parse-actual-tokens-from-error.js";
151
180
  import type { TraceEmitter } from "./trace-emitter.js";
181
+ import type { TrustContext } from "./trust-context.js";
152
182
  import { stripHistoricalWebSearchResults } from "./web-search-history.js";
153
183
 
154
184
  const log = getLogger("conversation-agent-loop");
@@ -171,77 +201,210 @@ type GitServiceInitializer = {
171
201
  ensureInitialized(): Promise<void>;
172
202
  };
173
203
 
174
- // ── Compaction circuit-breaker constants ────────────────────────────
204
+ // ── Compaction circuit-breaker pipeline helpers ─────────────────────
205
+ //
206
+ // The circuit-breaker behavior (3 consecutive summary-LLM failures trips a
207
+ // 1-hour cooldown) is now implemented by the `circuitBreaker` plugin
208
+ // pipeline. The default plugin (`plugins/defaults/circuit-breaker.ts`)
209
+ // replicates the legacy threshold/cooldown constants and event-emission
210
+ // semantics exactly — it operates on the `consecutiveCompactionFailures` /
211
+ // `compactionCircuitOpenUntil` fields the conversation still owns so the
212
+ // dev-only playground routes (`POST /playground/reset-compaction-circuit`,
213
+ // `POST /playground/inject-compaction-failures`) continue to read and
214
+ // mutate those fields directly.
175
215
  //
176
- // The circuit opens after `COMPACTION_CIRCUIT_FAILURE_THRESHOLD` consecutive
177
- // summary-LLM failures and stays open for `COMPACTION_CIRCUIT_COOLDOWN_MS`
178
- // before auto-compaction is allowed to retry. User-initiated compaction
179
- // (`force: true`) bypasses the breaker regardless of its state.
180
- const COMPACTION_CIRCUIT_FAILURE_THRESHOLD = 3;
181
- const COMPACTION_CIRCUIT_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour
216
+ // The helpers below build the pipeline inputs and invoke the runner. They
217
+ // are the sole entry points the rest of the daemon uses to query or update
218
+ // the compaction circuit.
219
+
220
+ /** Circuit-breaker key for a specific conversation's compaction pipeline. */
221
+ function compactionCircuitKey(conversationId: string): string {
222
+ return `compaction:${conversationId}`;
223
+ }
182
224
 
183
225
  /**
184
- * Check whether the compaction circuit breaker is currently open for the
185
- * given context. The breaker auto-closes once `compactionCircuitOpenUntil`
186
- * has elapsed.
226
+ * Build the minimal {@link TurnContext} the pipeline runner requires. Called
227
+ * both from inside the agent loop (where turn identifiers are available) and
228
+ * from non-turn invocations like `Conversation.forceCompact` (which falls
229
+ * back to stable placeholders so the runner's log records still carry the
230
+ * conversation identifier).
187
231
  */
188
- export function isCompactionCircuitOpen(ctx: {
189
- compactionCircuitOpenUntil: number | null;
190
- }): boolean {
191
- return (
192
- ctx.compactionCircuitOpenUntil !== null &&
193
- Date.now() < ctx.compactionCircuitOpenUntil
194
- );
232
+ function buildCircuitTurnContext(ctx: {
233
+ readonly conversationId: string;
234
+ currentRequestId?: string;
235
+ currentTurnTrustContext?: TrustContext;
236
+ trustContext?: TrustContext;
237
+ turnCount: number;
238
+ }): PluginTurnContext {
239
+ const trust: TrustContext =
240
+ ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
241
+ return {
242
+ requestId: ctx.currentRequestId ?? "circuit-breaker",
243
+ conversationId: ctx.conversationId,
244
+ turnIndex: ctx.turnCount,
245
+ trust,
246
+ };
195
247
  }
196
248
 
197
249
  /**
198
- * Track the outcome of a `maybeCompact()` call against the circuit breaker.
250
+ * Run the `circuitBreaker` pipeline for the compaction circuit on this
251
+ * conversation. When `outcome` is provided, state is updated (and transition
252
+ * events emit via `onEvent`); when omitted the call is query-only.
199
253
  *
200
- * - When the summary LLM call failed (local fallback covered the result),
201
- * increment the consecutive-failure counter. If the counter reaches the
202
- * threshold, open the circuit for the cooldown window and emit
203
- * `compaction_circuit_open` so clients can surface a notice.
204
- * - When the call did not fail, reset the counter and clear any open circuit.
254
+ * Returns the post-call decision from the pipeline. Callers gate auto-paths
255
+ * on `!result.open` and admit forced paths regardless of the decision.
256
+ */
257
+ async function runCompactionCircuitPipeline(
258
+ ctx: {
259
+ readonly conversationId: string;
260
+ consecutiveCompactionFailures: number;
261
+ compactionCircuitOpenUntil: number | null;
262
+ currentRequestId?: string;
263
+ currentTurnTrustContext?: TrustContext;
264
+ trustContext?: TrustContext;
265
+ turnCount: number;
266
+ },
267
+ args: {
268
+ outcome?: "success" | "failure";
269
+ onEvent?: (msg: ServerMessage) => void;
270
+ },
271
+ ): Promise<CircuitBreakerResult> {
272
+ const turnContext = buildCircuitTurnContext(ctx);
273
+ return runPipeline<CircuitBreakerArgs, CircuitBreakerResult>(
274
+ "circuitBreaker",
275
+ getMiddlewaresFor("circuitBreaker"),
276
+ async (terminalArgs) => {
277
+ // No plugin in the chain produced a decision. This should be
278
+ // unreachable in production because the default plugin registers a
279
+ // `circuitBreaker` middleware that always returns a decision, but we
280
+ // defensively derive the state here so test setups that intentionally
281
+ // omit the default plugin still get a sensible response.
282
+ const openUntil = terminalArgs.state.compactionCircuitOpenUntil;
283
+ const now = Date.now();
284
+ if (openUntil !== null && now < openUntil) {
285
+ return { open: true, cooldownRemainingMs: openUntil - now };
286
+ }
287
+ return { open: false };
288
+ },
289
+ {
290
+ key: compactionCircuitKey(ctx.conversationId),
291
+ // Pass the ctx directly as the mutable state container. The
292
+ // `CircuitBreakerArgs.state` shape deliberately matches the subset of
293
+ // fields the conversation owns so plugins mutate the same object the
294
+ // playground routes read and write.
295
+ state: ctx,
296
+ ...(args.outcome !== undefined ? { outcome: args.outcome } : {}),
297
+ ...(args.onEvent ? { onEvent: args.onEvent } : {}),
298
+ },
299
+ turnContext,
300
+ DEFAULT_TIMEOUTS.circuitBreaker,
301
+ );
302
+ }
303
+
304
+ /**
305
+ * Query-only: is the compaction circuit breaker currently open for this
306
+ * conversation? Thin wrapper around {@link runCompactionCircuitPipeline}
307
+ * with no outcome. Async because the pipeline runner is async, but the
308
+ * default plugin resolves synchronously on its microtask.
309
+ */
310
+ async function isCompactionCircuitOpen(ctx: {
311
+ readonly conversationId: string;
312
+ consecutiveCompactionFailures: number;
313
+ compactionCircuitOpenUntil: number | null;
314
+ currentRequestId?: string;
315
+ currentTurnTrustContext?: TrustContext;
316
+ trustContext?: TrustContext;
317
+ turnCount: number;
318
+ }): Promise<boolean> {
319
+ const decision = await runCompactionCircuitPipeline(ctx, {});
320
+ return decision.open;
321
+ }
322
+
323
+ /**
324
+ * Update the compaction circuit breaker with the outcome of a `maybeCompact`
325
+ * call and emit any transition event. A `summaryFailed` value of `undefined`
326
+ * means the summary LLM never ran (early return) — callers must guard with
327
+ * `summaryFailed !== undefined` before invoking this helper so early-return
328
+ * paths don't silently reset the 3-strike counter.
205
329
  *
206
- * This is called by every `maybeCompact()` site (including forced ones),
207
- * because a run of three failures is a provider-health signal regardless of
208
- * whether the caller bypassed the breaker.
330
+ * The default plugin handles threshold-based tripping and cooldown reset;
331
+ * see `plugins/defaults/circuit-breaker.ts` for the canonical semantics.
209
332
  */
210
- export function trackCompactionOutcome(
333
+ export async function trackCompactionOutcome(
211
334
  ctx: {
335
+ readonly conversationId: string;
212
336
  consecutiveCompactionFailures: number;
213
337
  compactionCircuitOpenUntil: number | null;
338
+ currentRequestId?: string;
339
+ currentTurnTrustContext?: TrustContext;
340
+ trustContext?: TrustContext;
341
+ turnCount: number;
214
342
  },
215
- summaryFailed: boolean | undefined,
343
+ summaryFailed: boolean,
216
344
  onEvent: (msg: ServerMessage) => void,
217
- ): void {
218
- if (summaryFailed) {
219
- ctx.consecutiveCompactionFailures += 1;
220
- // Treat a stale/expired open-until timestamp the same as null so a new
221
- // 3-strike window can re-open the circuit after the prior cooldown
222
- // elapses. Without this the second trip would no-op because
223
- // `compactionCircuitOpenUntil` remains set to a past timestamp even
224
- // though `isCompactionCircuitOpen()` correctly reports closed.
225
- const circuitDormant =
226
- ctx.compactionCircuitOpenUntil === null ||
227
- Date.now() >= ctx.compactionCircuitOpenUntil;
228
- if (
229
- ctx.consecutiveCompactionFailures >=
230
- COMPACTION_CIRCUIT_FAILURE_THRESHOLD &&
231
- circuitDormant
232
- ) {
233
- const openUntil = Date.now() + COMPACTION_CIRCUIT_COOLDOWN_MS;
234
- ctx.compactionCircuitOpenUntil = openUntil;
235
- onEvent({
236
- type: "compaction_circuit_open",
237
- reason: "3_consecutive_failures",
238
- openUntil,
239
- });
240
- }
241
- } else {
242
- ctx.consecutiveCompactionFailures = 0;
243
- ctx.compactionCircuitOpenUntil = null;
244
- }
345
+ ): Promise<void> {
346
+ await runCompactionCircuitPipeline(ctx, {
347
+ outcome: summaryFailed ? "failure" : "success",
348
+ onEvent,
349
+ });
350
+ }
351
+
352
+ // ── Plugin pipeline helpers ──────────────────────────────────────────
353
+ //
354
+ // Canonical {@link PluginTurnContext} builder threaded into every
355
+ // `runPipeline` call inside `runAgentLoopImpl`. The orchestrator composes
356
+ // the context on demand at each call site from ambient state rather than
357
+ // carrying a persistent `TurnContext` instance across the turn.
358
+
359
+ /**
360
+ * Synthetic fallback trust context used when the orchestrator fires a pipeline
361
+ * before the per-turn trust snapshot has been captured (e.g. invocations that
362
+ * bypass `processMessage` / `drainQueue`). We bias to `unknown` rather than
363
+ * `guardian` so a missing snapshot cannot accidentally grant elevated trust
364
+ * to a custom plugin reading `ctx.trust`.
365
+ */
366
+ const FALLBACK_TURN_TRUST: TrustContext = {
367
+ sourceChannel: "vellum",
368
+ trustClass: "unknown",
369
+ };
370
+
371
+ /**
372
+ * Build the {@link TurnContext} passed to {@link runPipeline}.
373
+ *
374
+ * Canonical source of truth for every pipeline call site inside the agent
375
+ * loop. Every `runPipeline` invocation in `runAgentLoopImpl` (and in the
376
+ * handlers that share its ambient state) must route through this helper
377
+ * rather than constructing a `TurnContext` literal inline — this keeps
378
+ * `turnIndex`, trust resolution, and the `contextWindowManager` attachment
379
+ * consistent across pipeline slots, which in turn keeps structured logs
380
+ * filtered by `conversationId`/`turnIndex` coherent across slots.
381
+ *
382
+ * Behavior:
383
+ * - `turnIndex` is always `ctx.turnCount` — the orchestrator-owned
384
+ * 0-based turn counter. Reading from a single source avoids the
385
+ * earlier inconsistency (`ctx.turnCount`, `ctx.messages.length - 1`,
386
+ * `ctx.messages.length`, and `0` were all used for the same turn).
387
+ * - Trust pulls from the per-turn snapshot first, then the conversation-
388
+ * level context, then {@link FALLBACK_TURN_TRUST}. The cascade matches
389
+ * the one inside the orchestrator's inline injection assembly so
390
+ * middleware reads the same trust class the runtime sees.
391
+ * - `contextWindowManager` is attached unconditionally. Pipelines that
392
+ * don't need it can ignore it; the default compaction plugin reads it
393
+ * via the typed optional field on `TurnContext`.
394
+ */
395
+ function buildPluginTurnContext(
396
+ ctx: AgentLoopConversationContext,
397
+ requestId: string,
398
+ ): PluginTurnContext {
399
+ const trust =
400
+ ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
401
+ return {
402
+ requestId,
403
+ conversationId: ctx.conversationId,
404
+ turnIndex: ctx.turnCount,
405
+ trust,
406
+ contextWindowManager: ctx.contextWindowManager,
407
+ };
245
408
  }
246
409
 
247
410
  // ── Context Interface ────────────────────────────────────────────────
@@ -305,6 +468,15 @@ export interface AgentLoopConversationContext {
305
468
  currentTurnTrustContext?: TrustContext;
306
469
  /** Per-turn snapshot of channelCapabilities, frozen at message-processing start. */
307
470
  currentTurnChannelCapabilities?: ChannelCapabilities;
471
+ /**
472
+ * Per-turn snapshot of the resolved inference-profile override. Read by
473
+ * `createToolExecutor` so `ToolContext.overrideProfile` carries the same
474
+ * profile the agent loop is sending to the provider. Without this, a tool
475
+ * that spawns nested subagents (e.g. `subagent_spawn`) cannot recover the
476
+ * override from a row read because the in-flight subagent's own row never
477
+ * had `inferenceProfile` set.
478
+ */
479
+ currentTurnOverrideProfile?: string;
308
480
  commandIntent?: { type: string; payload?: string; languageCode?: string };
309
481
  trustContext?: TrustContext;
310
482
  /** Task-run scope for the current turn. Cleared at turn end so queued/drained turns don't inherit it. */
@@ -361,13 +533,10 @@ export interface AgentLoopConversationContext {
361
533
  statusText?: string,
362
534
  ): void;
363
535
  emitConfirmationStateChanged(
364
- params: import("./message-types/messages.js").ConfirmationStateChanged extends {
536
+ params: ConfirmationStateChanged extends {
365
537
  type: infer _;
366
538
  }
367
- ? Omit<
368
- import("./message-types/messages.js").ConfirmationStateChanged,
369
- "type"
370
- >
539
+ ? Omit<ConfirmationStateChanged, "type">
371
540
  : never,
372
541
  ): void;
373
542
 
@@ -404,7 +573,6 @@ export async function runAgentLoopImpl(
404
573
  userMessageId: string,
405
574
  onEvent: (msg: ServerMessage) => void,
406
575
  options?: {
407
- skipPreMessageRollback?: boolean;
408
576
  isInteractive?: boolean;
409
577
  isUserMessage?: boolean;
410
578
  titleText?: string;
@@ -415,6 +583,16 @@ export async function runAgentLoopImpl(
415
583
  * the agent loop defaults to `'mainAgent'` for user-initiated turns.
416
584
  */
417
585
  callSite?: LLMCallSite;
586
+ /**
587
+ * Optional ad-hoc inference-profile override applied to every LLM call
588
+ * the loop issues. When set, the agent loop sets
589
+ * `SendMessageOptions.config.overrideProfile` on each provider call so
590
+ * the resolver layers `llm.profiles[<name>]` between the workspace's
591
+ * `activeProfile` and the call-site's named profile. Used by
592
+ * per-conversation pinned profiles (and inherited by subagents the loop
593
+ * spawns).
594
+ */
595
+ overrideProfile?: string;
418
596
  },
419
597
  ): Promise<void> {
420
598
  if (!ctx.abortController) {
@@ -445,6 +623,28 @@ export async function runAgentLoopImpl(
445
623
  // `llm.callSites.mainAgent` (falling back to `llm.default` when absent).
446
624
  const turnCallSite: LLMCallSite = options?.callSite ?? "mainAgent";
447
625
 
626
+ // Read the conversation row once for both the override-profile derivation
627
+ // below and the title-replaceability check at turn start. Later reads in
628
+ // this function (post-turn truncation, disk sync, home-feed emission)
629
+ // intentionally re-read because state can change during the turn.
630
+ const turnStartConversation = getConversation(ctx.conversationId);
631
+
632
+ // Optional per-turn inference-profile override. Plumbed through to every
633
+ // LLM call the loop emits and inherited by any subagents spawned during
634
+ // this turn. Caller-supplied `options.overrideProfile` (e.g.
635
+ // SubagentManager forwarding the parent's pinned profile into the
636
+ // spawned subagent's background conversation) wins over the row read
637
+ // so the agent loop's own background-skip rule doesn't zero out an
638
+ // explicitly inherited override.
639
+ const turnOverrideProfile =
640
+ options?.overrideProfile ??
641
+ getConversationOverrideProfileFromRow(turnStartConversation);
642
+
643
+ // Snapshot for `createToolExecutor` to read into `ToolContext.overrideProfile`
644
+ // — see field doc on `AgentLoopConversationContext` for why the tool needs
645
+ // it (nested subagent spawns can't recover the override from a row read).
646
+ ctx.currentTurnOverrideProfile = turnOverrideProfile;
647
+
448
648
  // Capture the turn channel context *before* any awaits so a second
449
649
  // message from a different channel can't overwrite it mid-flight.
450
650
  // When context is unavailable (e.g. regenerate after daemon restart),
@@ -475,8 +675,8 @@ export async function runAgentLoopImpl(
475
675
  assistantMessageInterface: origin,
476
676
  };
477
677
  return {
478
- userMessageInterface: "vellum" as InterfaceId,
479
- assistantMessageInterface: "vellum" as InterfaceId,
678
+ userMessageInterface: "web" as InterfaceId,
679
+ assistantMessageInterface: "web" as InterfaceId,
480
680
  };
481
681
  })();
482
682
 
@@ -528,59 +728,47 @@ export async function runAgentLoopImpl(
528
728
  }
529
729
  }
530
730
 
531
- const preMessageResult = await getHookManager().trigger("pre-message", {
532
- conversationId: ctx.conversationId,
533
- messagePreview: truncate(content, 200, ""),
534
- });
535
-
536
- if (preMessageResult.blocked) {
537
- if (!options?.skipPreMessageRollback) {
538
- ctx.messages.pop();
539
- deleteMessageById(userMessageId);
540
- }
541
- // Replace loading placeholder so the conversation isn't stuck as "Generating title..."
542
- const currentConv = getConversation(ctx.conversationId);
543
- if (
544
- isReplaceableTitle(currentConv?.title ?? null) &&
545
- currentConv?.title !== UNTITLED_FALLBACK
546
- ) {
547
- updateConversationTitle(ctx.conversationId, UNTITLED_FALLBACK);
548
- onEvent({
549
- type: "conversation_title_updated",
550
- conversationId: ctx.conversationId,
551
- title: UNTITLED_FALLBACK,
552
- });
553
- }
554
- onEvent({
555
- type: "error",
556
- message: `Message blocked by hook "${preMessageResult.blockedBy}"`,
557
- });
558
- return;
559
- }
560
-
561
731
  // Generate title early — the user message alone is sufficient context.
562
- // Firing after hook gating but before the main LLM call removes the
563
- // delay of waiting for the full assistant response. The second-pass
564
- // regeneration at turn 3 will refine the title with more context.
732
+ // Firing before the main LLM call removes the delay of waiting for the
733
+ // full assistant response. The second-pass regeneration at turn 3 will
734
+ // refine the title with more context.
565
735
  // No abort signal — title generation should complete even if the user
566
736
  // cancels the response, since the user message is already persisted.
567
737
  // Deferred via setTimeout so the main agent loop LLM call enqueues
568
738
  // first, avoiding rate-limit slot contention on strict configs.
569
- if (
570
- isReplaceableTitle(getConversation(ctx.conversationId)?.title ?? null)
571
- ) {
739
+ if (isReplaceableTitle(turnStartConversation?.title ?? null)) {
740
+ // TurnContext routed through the canonical builder so the pipeline's
741
+ // log record reports the same `conversationId`/`turnIndex` shape as
742
+ // every other slot in this turn. Title generation does not depend on
743
+ // the context-window manager attached by the builder, but sharing the
744
+ // builder keeps the invariant enforced in one place.
745
+ const titlePipelineCtx = buildPluginTurnContext(ctx, reqId);
746
+ const titleArgs = {
747
+ conversationId: ctx.conversationId,
748
+ provider: ctx.provider,
749
+ userMessage: options?.titleText ?? content,
750
+ onTitleUpdated: (title: string) => {
751
+ onEvent({
752
+ type: "conversation_title_updated",
753
+ conversationId: ctx.conversationId,
754
+ title,
755
+ });
756
+ },
757
+ };
572
758
  setTimeout(() => {
573
- queueGenerateConversationTitle({
574
- conversationId: ctx.conversationId,
575
- provider: ctx.provider,
576
- userMessage: options?.titleText ?? content,
577
- onTitleUpdated: (title) => {
578
- onEvent({
579
- type: "conversation_title_updated",
580
- conversationId: ctx.conversationId,
581
- title,
582
- });
583
- },
759
+ runPipeline(
760
+ "titleGenerate",
761
+ getMiddlewaresFor("titleGenerate"),
762
+ defaultTitleGenerateTerminal,
763
+ titleArgs,
764
+ titlePipelineCtx,
765
+ DEFAULT_TIMEOUTS.titleGenerate,
766
+ ).catch((err) => {
767
+ // Fire-and-forget — keep previous non-propagating semantics.
768
+ // queueGenerateConversationTitle already swallows internal
769
+ // errors; this catch covers pipeline-layer errors (timeouts,
770
+ // middleware throws) without surfacing them to the agent loop.
771
+ rlog.warn({ err }, "titleGenerate pipeline failed (non-fatal)");
584
772
  });
585
773
  }, 0);
586
774
  }
@@ -592,7 +780,7 @@ export async function runAgentLoopImpl(
592
780
  const compactCheck = ctx.contextWindowManager.shouldCompact(ctx.messages);
593
781
  // Skip auto-compaction while the circuit breaker is open. Force paths
594
782
  // and user-initiated /compact bypass this check.
595
- const autoCompactAllowed = !isCompactionCircuitOpen(ctx);
783
+ const autoCompactAllowed = !(await isCompactionCircuitOpen(ctx));
596
784
  if (compactCheck.needed && autoCompactAllowed) {
597
785
  ctx.emitActivityState(
598
786
  "thinking",
@@ -601,69 +789,59 @@ export async function runAgentLoopImpl(
601
789
  reqId,
602
790
  );
603
791
  }
604
- const compacted = autoCompactAllowed
605
- ? await ctx.contextWindowManager.maybeCompact(
606
- ctx.messages,
607
- abortController.signal,
792
+ let compacted: Awaited<
793
+ ReturnType<typeof ctx.contextWindowManager.maybeCompact>
794
+ > | null = null;
795
+ if (autoCompactAllowed) {
796
+ try {
797
+ compacted = (await runPipeline<CompactionArgs, CompactionResult>(
798
+ "compaction",
799
+ getMiddlewaresFor("compaction"),
800
+ (args) =>
801
+ defaultCompactionTerminal(args, buildPluginTurnContext(ctx, reqId)),
608
802
  {
609
- lastCompactedAt: ctx.contextCompactedAt ?? undefined,
610
- precomputedEstimate: compactCheck.estimatedTokens,
611
- conversationOriginChannel:
612
- getConversationOriginChannel(ctx.conversationId) ?? undefined,
803
+ messages: ctx.messages,
804
+ signal: abortController.signal,
805
+ options: {
806
+ lastCompactedAt: ctx.contextCompactedAt ?? undefined,
807
+ precomputedEstimate: compactCheck.estimatedTokens,
808
+ conversationOriginChannel:
809
+ getConversationOriginChannel(ctx.conversationId) ?? undefined,
810
+ },
613
811
  },
614
- )
615
- : null;
812
+ buildPluginTurnContext(ctx, reqId),
813
+ DEFAULT_TIMEOUTS.compaction,
814
+ )) as Awaited<ReturnType<typeof ctx.contextWindowManager.maybeCompact>>;
815
+ } catch (err) {
816
+ if (err instanceof PluginTimeoutError) {
817
+ // Pipeline exceeded its budget. Record the failure so the circuit
818
+ // breaker tracks consecutive timeouts (it trips after three),
819
+ // then degrade gracefully by skipping compaction this turn —
820
+ // the turn proceeds with the un-compacted history rather than
821
+ // hard-failing. The inner summary call has been aborted by the
822
+ // runner's signal-linking, so updateSummary's local fallback
823
+ // also ran before this catch block is reached.
824
+ rlog.warn(
825
+ { err, phase: "start-of-turn-compaction" },
826
+ "Compaction pipeline timed out — skipping compaction this turn",
827
+ );
828
+ await trackCompactionOutcome(ctx, true, onEvent);
829
+ compacted = null;
830
+ } else {
831
+ throw err;
832
+ }
833
+ }
834
+ }
616
835
  // Only track circuit-breaker state when a summary LLM call actually ran.
617
836
  // `summaryFailed` is `undefined` on early returns (compaction disabled,
618
837
  // below threshold, cooldown active, no eligible messages, truncation-only
619
838
  // path) — treating those as "successful" compactions would silently reset
620
839
  // the 3-strike counter and break the invariant.
621
840
  if (compacted && compacted.summaryFailed !== undefined) {
622
- trackCompactionOutcome(ctx, compacted.summaryFailed, onEvent);
841
+ await trackCompactionOutcome(ctx, compacted.summaryFailed, onEvent);
623
842
  }
624
843
  if (compacted?.compacted) {
625
- ctx.messages = compacted.messages;
626
- ctx.contextCompactedMessageCount += compacted.compactedPersistedMessages;
627
- ctx.contextCompactedAt = Date.now();
628
- // Notify memory graph that compaction happened — triggers full context
629
- // reload on the next turn to replenish lost memory context.
630
- ctx.graphMemory.onCompacted(compacted.compactedPersistedMessages);
631
- updateConversationContextWindow(
632
- ctx.conversationId,
633
- compacted.summaryText,
634
- ctx.contextCompactedMessageCount,
635
- );
636
- // Fire auto-analysis on compaction so the reflective agent can
637
- // crystallize anything worth remembering before the context window
638
- // narrows further.
639
- enqueueAutoAnalysisOnCompaction(
640
- ctx.conversationId,
641
- ctx.trustContext?.trustClass,
642
- );
643
- onEvent({
644
- type: "context_compacted",
645
- previousEstimatedInputTokens: compacted.previousEstimatedInputTokens,
646
- estimatedInputTokens: compacted.estimatedInputTokens,
647
- maxInputTokens: compacted.maxInputTokens,
648
- thresholdTokens: compacted.thresholdTokens,
649
- compactedMessages: compacted.compactedMessages,
650
- summaryCalls: compacted.summaryCalls,
651
- summaryInputTokens: compacted.summaryInputTokens,
652
- summaryOutputTokens: compacted.summaryOutputTokens,
653
- summaryModel: compacted.summaryModel,
654
- });
655
- emitUsage(
656
- ctx,
657
- compacted.summaryInputTokens,
658
- compacted.summaryOutputTokens,
659
- compacted.summaryModel,
660
- onEvent,
661
- "context_compactor",
662
- reqId,
663
- compacted.summaryCacheCreationInputTokens ?? 0,
664
- compacted.summaryCacheReadInputTokens ?? 0,
665
- collapseRawResponses(compacted.summaryRawResponses),
666
- );
844
+ applyCompactionResult(ctx, compacted, onEvent, reqId);
667
845
  shouldInjectWorkspace = true;
668
846
  if (compacted.compactedPersistedMessages > 0) {
669
847
  compactedThisTurn = true;
@@ -711,21 +889,58 @@ export async function runAgentLoopImpl(
711
889
 
712
890
  let runMessages = ctx.messages;
713
891
 
714
- // Memory graph retrieval — dispatches to context-load / per-turn based on
715
- // conversation state. Keep the query vector around so the PKB reminder
716
- // can reuse it for relevance-hint search (see `applyRuntimeInjections`).
717
- let pkbQueryVector: number[] | undefined;
718
- let pkbSparseVector:
719
- | import("../memory/qdrant-client.js").QdrantSparseVector
720
- | undefined;
892
+ // Memory retrieval pipeline fetches PKB, NOW.md, and memory-graph
893
+ // outputs through a single `memoryRetrieval` pipeline. Plugins may
894
+ // replace the terminal behavior by registering a middleware that
895
+ // short-circuits with its own `MemoryResult`; the default terminal
896
+ // below runs `runDefaultMemoryRetrieval` which reproduces the prior
897
+ // in-lined behavior (PKB/NOW reads + gated graph call).
721
898
  const isTrustedActor = resolveTrustClass(ctx.trustContext) === "guardian";
722
- if (isTrustedActor) {
723
- const graphResult = await ctx.graphMemory.prepareMemory(
724
- ctx.messages,
725
- getConfig(),
726
- abortController.signal,
727
- onEvent,
728
- );
899
+ // Canonical builder — pulls trust from per-turn snapshot, then
900
+ // conversation-level, then the synthetic fallback. Memory retrieval
901
+ // does not need the context-window handle the builder attaches, but
902
+ // keeping every call site on one helper is load-bearing for log
903
+ // coherence across pipeline slots.
904
+ const memoryPluginTurnCtx = buildPluginTurnContext(ctx, reqId);
905
+ const memoryArgs: MemoryArgs = {
906
+ conversationId: ctx.conversationId,
907
+ trustContext: ctx.trustContext,
908
+ turnIndex: ctx.turnCount,
909
+ // Pass the abort signal via `args` (not `deps`) so the pipeline
910
+ // runner's `linkAbortSignal` can swap it for a signal linked to the
911
+ // pipeline's internal controller — on a plugin-set timeout or
912
+ // external cancel, the linked signal aborts and `prepareMemory`
913
+ // stops mutating graph state / emitting events after the pipeline
914
+ // has already errored.
915
+ signal: abortController.signal,
916
+ };
917
+ const memoryDeps: DefaultMemoryRetrievalDeps = {
918
+ messages: ctx.messages,
919
+ graphMemory: ctx.graphMemory,
920
+ config: getConfig(),
921
+ onEvent,
922
+ isTrustedActor,
923
+ };
924
+ const memoryResult: MemoryResult = await runPipeline(
925
+ "memoryRetrieval",
926
+ getMiddlewaresFor("memoryRetrieval"),
927
+ (args) => runDefaultMemoryRetrieval(args, memoryDeps),
928
+ memoryArgs,
929
+ memoryPluginTurnCtx,
930
+ DEFAULT_TIMEOUTS.memoryRetrieval,
931
+ );
932
+
933
+ // Consume the memory-graph block when the default retriever emitted
934
+ // one. Custom plugins that substitute their own blocks without the
935
+ // default discriminator are expected to handle their own side effects
936
+ // (event emission, metric persistence) inside their middleware; this
937
+ // block short-circuits to the original no-op behavior in that case.
938
+ const defaultGraphPayload: GraphMemoryPayload | null =
939
+ asDefaultGraphPayload(memoryResult.memoryGraphBlocks);
940
+ let pkbQueryVector: number[] | undefined;
941
+ let pkbSparseVector: QdrantSparseVector | undefined;
942
+ if (defaultGraphPayload) {
943
+ const graphResult = defaultGraphPayload.result;
729
944
  runMessages = graphResult.runMessages;
730
945
  // Select dense+sparse as a matched pair so RRF fusion combines two
731
946
  // signals aligned to the same query text:
@@ -746,12 +961,24 @@ export async function runAgentLoopImpl(
746
961
 
747
962
  // Persist the injected block text in message metadata so it survives
748
963
  // conversation reloads (eviction, restart, fork). loadFromDb re-injects
749
- // from metadata.
964
+ // from metadata. Routed through the `persistence` pipeline so plugins
965
+ // can observe or override metadata updates alongside add/delete.
750
966
  if (graphResult.injectedBlockText) {
751
967
  try {
752
- updateMessageMetadata(userMessageId, {
753
- memoryInjectedBlock: graphResult.injectedBlockText,
754
- });
968
+ await runPipeline<PersistArgs, PersistResult>(
969
+ "persistence",
970
+ getMiddlewaresFor("persistence"),
971
+ defaultPersistenceTerminal,
972
+ {
973
+ op: "update",
974
+ messageId: userMessageId,
975
+ updates: {
976
+ memoryInjectedBlock: graphResult.injectedBlockText,
977
+ },
978
+ },
979
+ buildPluginTurnContext(ctx, reqId),
980
+ DEFAULT_TIMEOUTS.persistence,
981
+ );
755
982
  } catch (err) {
756
983
  rlog.warn(
757
984
  { err },
@@ -933,11 +1160,13 @@ export async function runAgentLoopImpl(
933
1160
  // Inject NOW.md and PKB content only on the first turn (or after
934
1161
  // compaction re-strips them). Old injections persist in history and
935
1162
  // are never stripped on normal turns — this preserves the cached prefix.
936
- const currentNowContent = readNowScratchpad();
1163
+ // PKB/NOW content is sourced from the `memoryRetrieval` pipeline above
1164
+ // so plugins can override either source without touching the agent loop.
1165
+ const currentNowContent = memoryResult.nowContent;
937
1166
  const shouldInjectNowAndPkb = isFirstMessage || compactedThisTurn;
938
1167
  const nowScratchpad = shouldInjectNowAndPkb ? currentNowContent : null;
939
1168
 
940
- const currentPkbContent = readPkbContext();
1169
+ const currentPkbContent = memoryResult.pkbContent;
941
1170
  const pkbContext = shouldInjectNowAndPkb ? currentPkbContent : null;
942
1171
  const pkbActive = currentPkbContent !== null;
943
1172
 
@@ -1030,12 +1259,19 @@ export async function runAgentLoopImpl(
1030
1259
 
1031
1260
  let currentInjectionMode: InjectionMode = "full";
1032
1261
 
1262
+ // Canonical per-turn TurnContext forwarded to the injector chain. The
1263
+ // per-turn injection inputs are built inside `applyRuntimeInjections`
1264
+ // from the `injectionOpts` bag; we only need to hand in identity +
1265
+ // trust here so third-party injectors see the real turn metadata.
1266
+ const injectionTurnCtx = buildPluginTurnContext(ctx, reqId);
1267
+
1033
1268
  const injection = await applyRuntimeInjections(runMessages, {
1034
1269
  ...injectionOpts,
1035
1270
  slackChronologicalMessages: reducerCompacted
1036
1271
  ? null
1037
1272
  : injectionOpts.slackChronologicalMessages,
1038
1273
  mode: currentInjectionMode,
1274
+ turnContext: injectionTurnCtx,
1039
1275
  });
1040
1276
  runMessages = injection.messages;
1041
1277
 
@@ -1043,11 +1279,14 @@ export async function runAgentLoopImpl(
1043
1279
  // reloads (eviction, restart, fork). loadFromDb re-injects from metadata.
1044
1280
  // Only the first call site persists — the overflow-recovery re-entry sites
1045
1281
  // send identical bytes and the tail row may not correspond to
1046
- // `userMessageId`. Both blocks are written in a single call to avoid
1282
+ // `userMessageId`. All blocks are written in a single call to avoid
1047
1283
  // doubling SQLite SELECT+UPDATE work on every turn.
1048
1284
  if (
1049
1285
  injection.blocks.unifiedTurnContext ||
1050
- injection.blocks.pkbSystemReminder
1286
+ injection.blocks.pkbSystemReminder ||
1287
+ injection.blocks.workspaceBlock ||
1288
+ injection.blocks.nowScratchpadBlock ||
1289
+ injection.blocks.pkbContextBlock
1051
1290
  ) {
1052
1291
  try {
1053
1292
  const metadataUpdates: Record<string, unknown> = {};
@@ -1059,7 +1298,28 @@ export async function runAgentLoopImpl(
1059
1298
  metadataUpdates.pkbSystemReminderBlock =
1060
1299
  injection.blocks.pkbSystemReminder;
1061
1300
  }
1062
- updateMessageMetadata(userMessageId, metadataUpdates);
1301
+ if (injection.blocks.workspaceBlock) {
1302
+ metadataUpdates.workspaceBlock = injection.blocks.workspaceBlock;
1303
+ }
1304
+ if (injection.blocks.nowScratchpadBlock) {
1305
+ metadataUpdates.nowScratchpadBlock =
1306
+ injection.blocks.nowScratchpadBlock;
1307
+ }
1308
+ if (injection.blocks.pkbContextBlock) {
1309
+ metadataUpdates.pkbContextBlock = injection.blocks.pkbContextBlock;
1310
+ }
1311
+ await runPipeline<PersistArgs, PersistResult>(
1312
+ "persistence",
1313
+ getMiddlewaresFor("persistence"),
1314
+ defaultPersistenceTerminal,
1315
+ {
1316
+ op: "update",
1317
+ messageId: userMessageId,
1318
+ updates: metadataUpdates,
1319
+ },
1320
+ buildPluginTurnContext(ctx, reqId),
1321
+ DEFAULT_TIMEOUTS.persistence,
1322
+ );
1063
1323
  } catch (err) {
1064
1324
  rlog.warn({ err }, "Failed to persist injection metadata (non-fatal)");
1065
1325
  }
@@ -1082,18 +1342,51 @@ export async function runAgentLoopImpl(
1082
1342
  let reducerState: ReducerState | undefined;
1083
1343
 
1084
1344
  const toolTokenBudget = ctx.agentLoop.getToolTokenBudget(runMessages);
1085
- // Canonical calibration key used at every `estimatePromptTokens` site in
1086
- // this function. Matches the key recorded by `handleUsage` for wrapper
1087
- // providers (OpenRouter routing to Anthropic key is `"anthropic"`).
1345
+ // Canonical calibration key passed to the `tokenEstimate` pipeline for
1346
+ // every preflight/mid-loop estimate, the overflow reducer config, and the
1347
+ // convergence-path `estimatePromptTokens` call. Matches the key recorded
1348
+ // by `handleUsage` for wrapper providers (OpenRouter routing to
1349
+ // Anthropic → key is `"anthropic"`).
1088
1350
  const estimationProviderName = getCalibrationProviderKey(ctx.provider);
1089
- const preflightTokens = estimatePromptTokens(
1090
- runMessages,
1091
- ctx.systemPrompt,
1092
- {
1093
- providerName: estimationProviderName,
1094
- toolTokenBudget,
1095
- },
1096
- );
1351
+
1352
+ // Shared `TurnContext` for every `tokenEstimate` pipeline invocation in
1353
+ // this turn. The pipeline is the extension point for plugins that want
1354
+ // to substitute an alternate estimator (e.g. provider-native tokenization)
1355
+ // without touching orchestrator code.
1356
+ //
1357
+ // Routed through the canonical builder — `turnIndex` is `ctx.turnCount`,
1358
+ // trust cascades through per-turn/conversation-level/fallback, and the
1359
+ // context-window handle rides along so any middleware that wants to
1360
+ // reuse the manager (e.g. to compute compaction-aware estimates) can.
1361
+ const pipelineTurnCtx = buildPluginTurnContext(ctx, reqId);
1362
+
1363
+ const runTokenEstimatePipeline = (
1364
+ history: Message[],
1365
+ ): Promise<EstimateResult> =>
1366
+ runPipeline<EstimateArgs, EstimateResult>(
1367
+ "tokenEstimate",
1368
+ getMiddlewaresFor("tokenEstimate"),
1369
+ defaultTokenEstimateTerminal,
1370
+ {
1371
+ // Shallow-frozen copies so a misbehaving middleware that mutates
1372
+ // `args.history` or `args.tools` in place (e.g. trims the array
1373
+ // before calling next) can't silently strip prompt context from
1374
+ // the orchestrator's live `runMessages` / resolved-tools arrays.
1375
+ // TypeScript `readonly` on `EstimateArgs` does not prevent
1376
+ // `push`/`splice` at runtime; the frozen wrapper throws in strict
1377
+ // mode and isolates any mutation attempts from the call-site state.
1378
+ history: Object.freeze([...history]) as Message[],
1379
+ systemPrompt: ctx.systemPrompt,
1380
+ tools: Object.freeze([
1381
+ ...ctx.agentLoop.getResolvedTools(history),
1382
+ ]) as ToolDefinition[],
1383
+ providerName: estimationProviderName,
1384
+ },
1385
+ pipelineTurnCtx,
1386
+ DEFAULT_TIMEOUTS.tokenEstimate,
1387
+ );
1388
+
1389
+ const preflightTokens = await runTokenEstimatePipeline(runMessages);
1097
1390
 
1098
1391
  if (overflowRecovery.enabled && preflightTokens > preflightBudget) {
1099
1392
  rlog.warn(
@@ -1105,163 +1398,246 @@ export async function runAgentLoopImpl(
1105
1398
  "Preflight budget exceeded — running overflow reducer before provider call",
1106
1399
  );
1107
1400
 
1108
- reducerState = createInitialReducerState();
1109
- let preflightAttempts = 0;
1110
-
1111
- while (
1112
- preflightAttempts < overflowRecovery.maxAttempts &&
1113
- !reducerState.exhausted
1114
- ) {
1115
- preflightAttempts++;
1116
- ctx.emitActivityState(
1117
- "thinking",
1118
- "context_compacting",
1119
- "assistant_turn",
1120
- reqId,
1121
- );
1122
- const step = await reduceContextOverflow(
1123
- ctx.messages,
1124
- {
1125
- providerName: estimationProviderName,
1126
- systemPrompt: ctx.systemPrompt,
1127
- contextWindow: config.llm.default.contextWindow,
1128
- targetTokens: preflightBudget,
1129
- toolTokenBudget,
1130
- },
1131
- reducerState,
1132
- (msgs, signal, opts) =>
1133
- ctx.contextWindowManager.maybeCompact(msgs, signal!, opts),
1134
- abortController.signal,
1135
- );
1136
-
1137
- reducerState = step.state;
1138
- ctx.messages = step.messages;
1139
- currentInjectionMode = step.state.injectionMode;
1140
-
1141
- // Track circuit-breaker state whenever the reducer invoked compaction.
1142
- // The reducer's forced_compaction tier uses force:true, so it bypasses
1143
- // the open-circuit check, but we still want failure tracking to detect
1144
- // a run of broken summaries and clear the counter on success. Only
1145
- // track when the summary LLM actually ran — `summaryFailed === undefined`
1146
- // indicates an early return (no eligible messages, truncation-only
1147
- // path, etc.) that shouldn't influence the breaker.
1148
- if (
1149
- step.compactionResult &&
1150
- step.compactionResult.summaryFailed !== undefined
1151
- ) {
1152
- trackCompactionOutcome(
1153
- ctx,
1154
- step.compactionResult.summaryFailed,
1155
- onEvent,
1156
- );
1157
- }
1158
-
1159
- if (step.compactionResult?.compacted) {
1160
- ctx.contextCompactedMessageCount +=
1161
- step.compactionResult.compactedPersistedMessages;
1162
- ctx.contextCompactedAt = Date.now();
1163
- updateConversationContextWindow(
1164
- ctx.conversationId,
1165
- step.compactionResult.summaryText,
1166
- ctx.contextCompactedMessageCount,
1167
- );
1168
- // Fire auto-analysis on compaction — see forceCompact() for rationale.
1169
- enqueueAutoAnalysisOnCompaction(
1170
- ctx.conversationId,
1171
- ctx.trustContext?.trustClass,
1172
- );
1173
- onEvent({
1174
- type: "context_compacted",
1175
- previousEstimatedInputTokens:
1176
- step.compactionResult.previousEstimatedInputTokens,
1177
- estimatedInputTokens: step.compactionResult.estimatedInputTokens,
1178
- maxInputTokens: step.compactionResult.maxInputTokens,
1179
- thresholdTokens: step.compactionResult.thresholdTokens,
1180
- compactedMessages: step.compactionResult.compactedMessages,
1181
- summaryCalls: step.compactionResult.summaryCalls,
1182
- summaryInputTokens: step.compactionResult.summaryInputTokens,
1183
- summaryOutputTokens: step.compactionResult.summaryOutputTokens,
1184
- summaryModel: step.compactionResult.summaryModel,
1185
- });
1186
- emitUsage(
1187
- ctx,
1188
- step.compactionResult.summaryInputTokens,
1189
- step.compactionResult.summaryOutputTokens,
1190
- step.compactionResult.summaryModel,
1191
- onEvent,
1192
- "context_compactor",
1401
+ // Overflow reduction runs through the plugin pipeline. The default
1402
+ // middleware (`default-overflow-reduce`, registered at bootstrap)
1403
+ // contains the historical tier loop — forced compaction → tool-result
1404
+ // truncation → media stubbing → injection downgrade — plus the
1405
+ // re-inject/re-estimate convergence check. The callbacks below are
1406
+ // the orchestrator-specific side effects that the plugin coordinates
1407
+ // per iteration (activity emission, compaction application, runtime
1408
+ // injection reassembly, token re-estimation). Registered plugins that
1409
+ // wrap the `overflowReduce` slot see each iteration through their own
1410
+ // middleware `next` callback.
1411
+ const overflowArgs: OverflowReduceArgs = {
1412
+ messages: ctx.messages,
1413
+ runMessages,
1414
+ systemPrompt: ctx.systemPrompt,
1415
+ providerName: estimationProviderName,
1416
+ contextWindow: config.llm.default.contextWindow,
1417
+ preflightBudget,
1418
+ toolTokenBudget,
1419
+ maxAttempts: overflowRecovery.maxAttempts,
1420
+ abortSignal: abortController.signal,
1421
+ compactFn: async (msgs, signal, opts) => {
1422
+ // Route the reducer's forced-compaction tier through the
1423
+ // `compaction` pipeline so registered plugins observe these
1424
+ // invocations. Without this, custom compaction middleware only
1425
+ // sees the three orchestrator-owned call sites and misses the
1426
+ // reducer-initiated forced compactions entirely.
1427
+ //
1428
+ // Pipeline timeouts must be caught locally — a `PluginTimeoutError`
1429
+ // bubbling out of here would abort the overflow-reducer tier loop
1430
+ // entirely, skipping fallback tiers (tool-result truncation, media
1431
+ // stubbing, injection downgrade) and bypassing circuit-breaker
1432
+ // bookkeeping. On timeout, record the failure and return a
1433
+ // `compacted: false` result so the reducer falls through to the
1434
+ // next tier.
1435
+ try {
1436
+ return (await runPipeline<CompactionArgs, CompactionResult>(
1437
+ "compaction",
1438
+ getMiddlewaresFor("compaction"),
1439
+ (args) =>
1440
+ defaultCompactionTerminal(
1441
+ args,
1442
+ buildPluginTurnContext(ctx, reqId),
1443
+ ),
1444
+ {
1445
+ messages: msgs,
1446
+ signal,
1447
+ options: opts,
1448
+ },
1449
+ buildPluginTurnContext(ctx, reqId),
1450
+ DEFAULT_TIMEOUTS.compaction,
1451
+ )) as Awaited<
1452
+ ReturnType<typeof ctx.contextWindowManager.maybeCompact>
1453
+ >;
1454
+ } catch (err) {
1455
+ if (err instanceof PluginTimeoutError) {
1456
+ rlog.warn(
1457
+ { err, phase: "overflow-reducer-forced-compaction" },
1458
+ "Compaction pipeline timed out — falling through to next reducer tier",
1459
+ );
1460
+ await trackCompactionOutcome(ctx, true, onEvent);
1461
+ return {
1462
+ messages: msgs,
1463
+ compacted: false,
1464
+ previousEstimatedInputTokens: 0,
1465
+ estimatedInputTokens: 0,
1466
+ maxInputTokens: 0,
1467
+ thresholdTokens: 0,
1468
+ compactedMessages: 0,
1469
+ compactedPersistedMessages: 0,
1470
+ summaryCalls: 0,
1471
+ summaryInputTokens: 0,
1472
+ summaryOutputTokens: 0,
1473
+ summaryModel: "",
1474
+ summaryText: "",
1475
+ reason: "compaction pipeline timed out",
1476
+ };
1477
+ }
1478
+ throw err;
1479
+ }
1480
+ },
1481
+ emitActivityState: () => {
1482
+ ctx.emitActivityState(
1483
+ "thinking",
1484
+ "context_compacting",
1485
+ "assistant_turn",
1193
1486
  reqId,
1194
- step.compactionResult.summaryCacheCreationInputTokens ?? 0,
1195
- step.compactionResult.summaryCacheReadInputTokens ?? 0,
1196
- collapseRawResponses(step.compactionResult.summaryRawResponses),
1197
- );
1198
- ctx.graphMemory.onCompacted(
1199
- step.compactionResult.compactedPersistedMessages,
1200
1487
  );
1201
- shouldInjectWorkspace = true;
1202
- reducerCompacted = true;
1203
- }
1204
-
1205
- // Re-inject with potentially downgraded injection mode.
1206
- // When compaction ran it strips existing NOW.md / PKB blocks, so we
1207
- // must re-inject the current content. Otherwise rely on the deduplicated
1208
- // value from injectionOpts to avoid duplicate injection.
1209
- const injection = await applyRuntimeInjections(ctx.messages, {
1210
- ...injectionOpts,
1211
- ...(step.compactionResult?.compacted && {
1212
- pkbContext: currentPkbContent,
1213
- }),
1214
- ...(step.compactionResult?.compacted && {
1215
- nowScratchpad: currentNowContent,
1216
- }),
1217
- workspaceTopLevelContext: shouldInjectWorkspace
1218
- ? ctx.workspaceTopLevelContext
1219
- : null,
1220
- // Once the reducer has compacted `ctx.messages`, the captured
1221
- // `slackChronologicalMessages` snapshot (built from the full
1222
- // persisted transcript) would overwrite the compacted history
1223
- // and undo compaction. Suppress the override from here on.
1224
- slackChronologicalMessages: reducerCompacted
1225
- ? null
1226
- : injectionOpts.slackChronologicalMessages,
1227
- mode: currentInjectionMode,
1228
- });
1229
- runMessages = injection.messages;
1230
- if (isTrustedActor && currentInjectionMode !== "minimal") {
1231
- const memResult = ctx.graphMemory.reinjectCachedMemory(runMessages);
1232
- runMessages = memResult.runMessages;
1233
- }
1234
-
1235
- // Re-estimate with injections included — step.estimatedTokens was
1236
- // computed on bare history (ctx.messages) and doesn't account for
1237
- // tokens added by runtime injections.
1238
- const postInjectionTokens = estimatePromptTokens(
1239
- runMessages,
1240
- ctx.systemPrompt,
1241
- {
1488
+ },
1489
+ onCompactionResult: async (result) => {
1490
+ // Track circuit-breaker state whenever the reducer invoked
1491
+ // compaction. The reducer's forced_compaction tier uses
1492
+ // force:true, so it bypasses the open-circuit check, but we
1493
+ // still want failure tracking to detect a run of broken
1494
+ // summaries and clear the counter on success. Only track when
1495
+ // the summary LLM actually ran `summaryFailed === undefined`
1496
+ // indicates an early return (no eligible messages,
1497
+ // truncation-only path, etc.) that shouldn't influence the
1498
+ // breaker.
1499
+ if (result.summaryFailed !== undefined) {
1500
+ await trackCompactionOutcome(ctx, result.summaryFailed, onEvent);
1501
+ }
1502
+ if (result.compacted) {
1503
+ applyCompactionResult(ctx, result, onEvent, reqId);
1504
+ shouldInjectWorkspace = true;
1505
+ }
1506
+ },
1507
+ reinjectForMode: async (
1508
+ reducedMessages,
1509
+ mode,
1510
+ stepCompacted,
1511
+ accumulatedCompacted,
1512
+ ) => {
1513
+ // Mirror the pre-PR-23 behavior: `ctx.messages` must track the
1514
+ // reducer's latest output before re-injection runs, because other
1515
+ // sites consulted through `injectionOpts` (`workspaceTopLevelContext`,
1516
+ // slack history, etc.) depend on it and `applyCompactionResult`
1517
+ // only updates `ctx.messages` on a compaction tier. Assigning here
1518
+ // keeps non-compaction tiers (tool-result truncation, media
1519
+ // stubbing, injection downgrade) observable to downstream
1520
+ // injection assembly on the same turn.
1521
+ ctx.messages = reducedMessages;
1522
+
1523
+ // When THIS iteration compacted, it stripped existing NOW.md /
1524
+ // PKB blocks so we re-inject current content. A later iteration
1525
+ // that only truncates or downgrades must NOT re-force PKB/NOW,
1526
+ // or each round would grow the token count.
1527
+ // Gate: only the iteration that actually compacted re-injects.
1528
+ const injection = await applyRuntimeInjections(reducedMessages, {
1529
+ ...injectionOpts,
1530
+ ...(stepCompacted && { pkbContext: currentPkbContent }),
1531
+ ...(stepCompacted && { nowScratchpad: currentNowContent }),
1532
+ workspaceTopLevelContext: shouldInjectWorkspace
1533
+ ? ctx.workspaceTopLevelContext
1534
+ : null,
1535
+ // Once ANY iteration has compacted `ctx.messages`, the captured
1536
+ // `slackChronologicalMessages` snapshot (built from the full
1537
+ // persisted transcript) would overwrite the compacted history
1538
+ // and undo compaction. Suppress the override from here on —
1539
+ // sticky across subsequent non-compacting iterations.
1540
+ slackChronologicalMessages: accumulatedCompacted
1541
+ ? null
1542
+ : injectionOpts.slackChronologicalMessages,
1543
+ mode,
1544
+ turnContext: buildPluginTurnContext(ctx, reqId),
1545
+ });
1546
+ let next = injection.messages;
1547
+ if (isTrustedActor && mode !== "minimal") {
1548
+ const memResult = ctx.graphMemory.reinjectCachedMemory(next);
1549
+ next = memResult.runMessages;
1550
+ }
1551
+ return next;
1552
+ },
1553
+ estimatePostInjection: (runMsgs) =>
1554
+ estimatePromptTokens(runMsgs, ctx.systemPrompt, {
1242
1555
  providerName: estimationProviderName,
1243
1556
  toolTokenBudget,
1244
- },
1245
- );
1557
+ }),
1558
+ };
1246
1559
 
1247
- if (postInjectionTokens <= preflightBudget) break;
1560
+ const overflowResult = await runPipeline<
1561
+ OverflowReduceArgs,
1562
+ OverflowReduceResult
1563
+ >(
1564
+ "overflowReduce",
1565
+ getMiddlewaresFor("overflowReduce"),
1566
+ // Terminal — only reached when every registered middleware calls
1567
+ // `next` and delegates past the innermost layer. The default plugin
1568
+ // is a terminal itself (it doesn't call `next`), so in practice
1569
+ // this fallback fires only when the default has been explicitly
1570
+ // deregistered (tests) and no user plugin replaces it. Strict-fail
1571
+ // semantics: throw so the missing terminal surfaces as a visible
1572
+ // error instead of silently returning the history untouched.
1573
+ async () => {
1574
+ throw new PluginExecutionError(
1575
+ "overflowReduce pipeline has no terminal handler — every reducer middleware called next() without providing a replacement",
1576
+ "overflowReduce",
1577
+ );
1578
+ },
1579
+ overflowArgs,
1580
+ buildPluginTurnContext(ctx, reqId),
1581
+ DEFAULT_TIMEOUTS.overflowReduce,
1582
+ );
1583
+
1584
+ ctx.messages = overflowResult.messages;
1585
+ runMessages = overflowResult.runMessages;
1586
+ currentInjectionMode = overflowResult.injectionMode;
1587
+ reducerState = overflowResult.reducerState;
1588
+ if (overflowResult.reducerCompacted) {
1589
+ reducerCompacted = true;
1248
1590
  }
1249
1591
  }
1250
1592
 
1251
- // Pre-run repair
1593
+ // Pre-run repair — routed through the `historyRepair` plugin pipeline so
1594
+ // plugins can observe or override repair behavior. The default plugin's
1595
+ // middleware is a passthrough; the actual repair runs in the terminal
1596
+ // (`defaultHistoryRepairTerminal`).
1252
1597
  let preRepairMessages = runMessages;
1253
- const preRunRepair = repairHistory(runMessages);
1254
- if (
1255
- preRunRepair.stats.assistantToolResultsMigrated > 0 ||
1256
- preRunRepair.stats.missingToolResultsInserted > 0 ||
1257
- preRunRepair.stats.orphanToolResultsDowngraded > 0 ||
1258
- preRunRepair.stats.consecutiveSameRoleMerged > 0
1259
- ) {
1260
- rlog.warn(
1261
- { phase: "pre_run", ...preRunRepair.stats },
1262
- "Repaired runtime history before provider call",
1598
+ let preRunRepair: HistoryRepairResult | null = null;
1599
+ try {
1600
+ preRunRepair = await runPipeline<HistoryRepairArgs, HistoryRepairResult>(
1601
+ "historyRepair",
1602
+ getMiddlewaresFor("historyRepair"),
1603
+ async (args) => defaultHistoryRepairTerminal(args),
1604
+ { history: runMessages, provider: ctx.provider.name },
1605
+ buildPluginTurnContext(ctx, reqId),
1606
+ DEFAULT_TIMEOUTS.historyRepair,
1263
1607
  );
1608
+ } catch (err) {
1609
+ if (err instanceof PluginTimeoutError) {
1610
+ // Pipeline exceeded its budget — likely a misbehaving third-party
1611
+ // middleware. Degrade gracefully by proceeding with the un-repaired
1612
+ // history rather than turn-fatal-erroring; un-repaired history is
1613
+ // strictly better than no turn at all, and the provider call itself
1614
+ // will still error visibly if the drift is unrecoverable.
1615
+ rlog.warn(
1616
+ { err, phase: "pre_run" },
1617
+ "historyRepair pipeline timed out — proceeding with un-repaired history",
1618
+ );
1619
+ } else {
1620
+ throw err;
1621
+ }
1622
+ }
1623
+ if (preRunRepair !== null) {
1624
+ // Always adopt the pipeline's output history — a user `historyRepair`
1625
+ // middleware may rewrite `messages` (e.g. provider-specific
1626
+ // normalization) without incrementing any of the built-in repair
1627
+ // counters. Gating the assignment on `stats` would silently discard
1628
+ // those edits and send the un-rewritten history to the provider.
1264
1629
  runMessages = preRunRepair.messages;
1630
+ if (
1631
+ preRunRepair.stats.assistantToolResultsMigrated > 0 ||
1632
+ preRunRepair.stats.missingToolResultsInserted > 0 ||
1633
+ preRunRepair.stats.orphanToolResultsDowngraded > 0 ||
1634
+ preRunRepair.stats.consecutiveSameRoleMerged > 0
1635
+ ) {
1636
+ rlog.warn(
1637
+ { phase: "pre_run", ...preRunRepair.stats },
1638
+ "Repaired runtime history before provider call",
1639
+ );
1640
+ }
1265
1641
  }
1266
1642
 
1267
1643
  // Replace historical web_search_tool_result blocks with text summaries.
@@ -1299,7 +1675,9 @@ export async function runAgentLoopImpl(
1299
1675
 
1300
1676
  let yieldedForBudget = false;
1301
1677
 
1302
- const onCheckpoint = (checkpoint: CheckpointInfo): CheckpointDecision => {
1678
+ const onCheckpoint = async (
1679
+ checkpoint: CheckpointInfo,
1680
+ ): Promise<CheckpointDecision> => {
1303
1681
  state.currentTurnToolNames = [];
1304
1682
 
1305
1683
  if (ctx.canHandoffAtCheckpoint()) {
@@ -1312,14 +1690,7 @@ export async function runAgentLoopImpl(
1312
1690
  // conversation-agent-loop run compaction before the provider rejects.
1313
1691
  if (overflowRecovery.enabled) {
1314
1692
  const midLoopThreshold = preflightBudget * 0.85;
1315
- const estimated = estimatePromptTokens(
1316
- checkpoint.history,
1317
- ctx.systemPrompt,
1318
- {
1319
- providerName: estimationProviderName,
1320
- toolTokenBudget,
1321
- },
1322
- );
1693
+ const estimated = await runTokenEstimatePipeline(checkpoint.history);
1323
1694
  if (estimated > midLoopThreshold) {
1324
1695
  rlog.warn(
1325
1696
  { phase: "mid-loop", estimated, threshold: midLoopThreshold },
@@ -1335,10 +1706,16 @@ export async function runAgentLoopImpl(
1335
1706
 
1336
1707
  turnStarted = true;
1337
1708
 
1338
- let denyCompressionMessage: Message | null = null;
1339
-
1340
1709
  rlog.info({ callSite: turnCallSite }, "Starting agent loop run");
1341
1710
 
1711
+ // Thread the orchestrator's canonical per-turn context into the agent
1712
+ // loop so its internal pipeline invocations (llmCall, emptyResponse,
1713
+ // toolError, toolResultTruncate, toolExecute) see the real
1714
+ // conversation identity / trust / contextWindowManager instead of the
1715
+ // synthesized `"agent-loop"` placeholder. The loop clones this value
1716
+ // and overwrites `turnIndex` with its own tool-use iteration counter.
1717
+ const loopTurnCtx = buildPluginTurnContext(ctx, reqId);
1718
+
1342
1719
  let updatedHistory = await ctx.agentLoop.run(
1343
1720
  runMessages,
1344
1721
  eventHandler,
@@ -1346,6 +1723,8 @@ export async function runAgentLoopImpl(
1346
1723
  reqId,
1347
1724
  onCheckpoint,
1348
1725
  turnCallSite,
1726
+ loopTurnCtx,
1727
+ turnOverrideProfile,
1349
1728
  );
1350
1729
 
1351
1730
  rlog.info(
@@ -1379,11 +1758,11 @@ export async function runAgentLoopImpl(
1379
1758
  const rawHistory = stripInjectionsForCompaction(updatedHistory);
1380
1759
  ctx.messages = rawHistory;
1381
1760
  try {
1382
- clearPkbSystemReminderMetadataForConversation(ctx.conversationId);
1761
+ clearStrippedInjectionMetadataForConversation(ctx.conversationId);
1383
1762
  } catch (err) {
1384
1763
  rlog.warn(
1385
1764
  { err },
1386
- "Failed to clear pkbSystemReminderBlock metadata after compaction strip (non-fatal)",
1765
+ "Failed to clear stripped-injection metadata after compaction strip (non-fatal)",
1387
1766
  );
1388
1767
  }
1389
1768
 
@@ -1394,65 +1773,61 @@ export async function runAgentLoopImpl(
1394
1773
  reqId,
1395
1774
  "Compacting context",
1396
1775
  );
1397
- const midLoopCompact = await ctx.contextWindowManager.maybeCompact(
1398
- ctx.messages,
1399
- abortController.signal,
1400
- {
1401
- lastCompactedAt: ctx.contextCompactedAt ?? undefined,
1402
- force: true,
1403
- targetInputTokensOverride: preflightBudget,
1404
- conversationOriginChannel:
1405
- getConversationOriginChannel(ctx.conversationId) ?? undefined,
1406
- },
1407
- );
1776
+ let midLoopCompact: Awaited<
1777
+ ReturnType<typeof ctx.contextWindowManager.maybeCompact>
1778
+ >;
1779
+ try {
1780
+ midLoopCompact = (await runPipeline<CompactionArgs, CompactionResult>(
1781
+ "compaction",
1782
+ getMiddlewaresFor("compaction"),
1783
+ (args) =>
1784
+ defaultCompactionTerminal(args, buildPluginTurnContext(ctx, reqId)),
1785
+ {
1786
+ messages: ctx.messages,
1787
+ signal: abortController.signal,
1788
+ options: {
1789
+ lastCompactedAt: ctx.contextCompactedAt ?? undefined,
1790
+ force: true,
1791
+ targetInputTokensOverride: preflightBudget,
1792
+ conversationOriginChannel:
1793
+ getConversationOriginChannel(ctx.conversationId) ?? undefined,
1794
+ },
1795
+ },
1796
+ buildPluginTurnContext(ctx, reqId),
1797
+ DEFAULT_TIMEOUTS.compaction,
1798
+ )) as Awaited<ReturnType<typeof ctx.contextWindowManager.maybeCompact>>;
1799
+ } catch (err) {
1800
+ if (err instanceof PluginTimeoutError) {
1801
+ // Mid-loop compaction timed out. Record the failure for the
1802
+ // circuit breaker and escalate to the convergence loop's more
1803
+ // aggressive reducer tiers (tool-result truncation, media
1804
+ // stubbing, injection downgrade) by flipping the overflow flag
1805
+ // and breaking out of the mid-loop retry. The existing
1806
+ // "exhausted all attempts" block further down handles the
1807
+ // escalation.
1808
+ rlog.warn(
1809
+ { err, phase: "mid-loop-compact" },
1810
+ "Compaction pipeline timed out — escalating to convergence loop",
1811
+ );
1812
+ await trackCompactionOutcome(ctx, true, onEvent);
1813
+ state.contextTooLargeDetected = true;
1814
+ break;
1815
+ }
1816
+ throw err;
1817
+ }
1408
1818
  // `force: true` bypasses the cooldown/threshold gates but early returns
1409
1819
  // for "no eligible messages" / "insufficient messages" still leave
1410
1820
  // `summaryFailed` undefined. Only track when the summary LLM actually ran.
1411
1821
  if (midLoopCompact.summaryFailed !== undefined) {
1412
- trackCompactionOutcome(ctx, midLoopCompact.summaryFailed, onEvent);
1413
- }
1414
- if (midLoopCompact.compacted) {
1415
- ctx.messages = midLoopCompact.messages;
1416
- reducerCompacted = true;
1417
- ctx.contextCompactedMessageCount +=
1418
- midLoopCompact.compactedPersistedMessages;
1419
- ctx.contextCompactedAt = Date.now();
1420
- updateConversationContextWindow(
1421
- ctx.conversationId,
1422
- midLoopCompact.summaryText,
1423
- ctx.contextCompactedMessageCount,
1424
- );
1425
- // Fire auto-analysis on compaction — see forceCompact() for rationale.
1426
- enqueueAutoAnalysisOnCompaction(
1427
- ctx.conversationId,
1428
- ctx.trustContext?.trustClass,
1429
- );
1430
- onEvent({
1431
- type: "context_compacted",
1432
- previousEstimatedInputTokens:
1433
- midLoopCompact.previousEstimatedInputTokens,
1434
- estimatedInputTokens: midLoopCompact.estimatedInputTokens,
1435
- maxInputTokens: midLoopCompact.maxInputTokens,
1436
- thresholdTokens: midLoopCompact.thresholdTokens,
1437
- compactedMessages: midLoopCompact.compactedMessages,
1438
- summaryCalls: midLoopCompact.summaryCalls,
1439
- summaryInputTokens: midLoopCompact.summaryInputTokens,
1440
- summaryOutputTokens: midLoopCompact.summaryOutputTokens,
1441
- summaryModel: midLoopCompact.summaryModel,
1442
- });
1443
- emitUsage(
1822
+ await trackCompactionOutcome(
1444
1823
  ctx,
1445
- midLoopCompact.summaryInputTokens,
1446
- midLoopCompact.summaryOutputTokens,
1447
- midLoopCompact.summaryModel,
1824
+ midLoopCompact.summaryFailed,
1448
1825
  onEvent,
1449
- "context_compactor",
1450
- reqId,
1451
- midLoopCompact.summaryCacheCreationInputTokens ?? 0,
1452
- midLoopCompact.summaryCacheReadInputTokens ?? 0,
1453
- collapseRawResponses(midLoopCompact.summaryRawResponses),
1454
1826
  );
1455
- ctx.graphMemory.onCompacted(midLoopCompact.compactedPersistedMessages);
1827
+ }
1828
+ if (midLoopCompact.compacted) {
1829
+ applyCompactionResult(ctx, midLoopCompact, onEvent, reqId);
1830
+ reducerCompacted = true;
1456
1831
  shouldInjectWorkspace = true;
1457
1832
  }
1458
1833
 
@@ -1474,6 +1849,7 @@ export async function runAgentLoopImpl(
1474
1849
  ? null
1475
1850
  : injectionOpts.slackChronologicalMessages,
1476
1851
  mode: currentInjectionMode,
1852
+ turnContext: buildPluginTurnContext(ctx, reqId),
1477
1853
  });
1478
1854
  runMessages = injection.messages;
1479
1855
  if (isTrustedActor && currentInjectionMode !== "minimal") {
@@ -1497,6 +1873,8 @@ export async function runAgentLoopImpl(
1497
1873
  reqId,
1498
1874
  onCheckpoint,
1499
1875
  turnCallSite,
1876
+ loopTurnCtx,
1877
+ turnOverrideProfile,
1500
1878
  );
1501
1879
  }
1502
1880
 
@@ -1526,6 +1904,15 @@ export async function runAgentLoopImpl(
1526
1904
  { phase: "retry" },
1527
1905
  "Provider ordering error detected, attempting one-shot deep-repair retry",
1528
1906
  );
1907
+ // Design note: deep-repair intentionally bypasses the `historyRepair`
1908
+ // plugin pipeline. Deep-repair is a recovery-only path triggered by a
1909
+ // provider ordering error — it must be deterministic and unaffected by
1910
+ // user middleware that might have caused (or be unable to recover from)
1911
+ // the original drift. Plugins can already observe / override the
1912
+ // pre-run repair via the `historyRepair` pipeline above; widening that
1913
+ // surface to deep-repair is intentionally deferred until there's a
1914
+ // concrete plugin-level use case. Do not route this call through
1915
+ // `runPipeline` without first revisiting that contract.
1529
1916
  const retryRepair = deepRepairHistory(runMessages);
1530
1917
  runMessages = retryRepair.messages;
1531
1918
  const retryStrip = stripHistoricalWebSearchResults(runMessages);
@@ -1542,6 +1929,8 @@ export async function runAgentLoopImpl(
1542
1929
  reqId,
1543
1930
  onCheckpoint,
1544
1931
  turnCallSite,
1932
+ loopTurnCtx,
1933
+ turnOverrideProfile,
1545
1934
  );
1546
1935
 
1547
1936
  if (state.orderingErrorDetected) {
@@ -1555,8 +1944,7 @@ export async function runAgentLoopImpl(
1555
1944
  // ── Bounded context overflow convergence loop ──────────────────
1556
1945
  // When the provider rejects with context-too-large, iterate through
1557
1946
  // reducer tiers (forced compaction, tool-result truncation, media
1558
- // stubbing, injection downgrade) with optional approval gating for
1559
- // interactive latest-turn compression.
1947
+ // stubbing, injection downgrade).
1560
1948
  //
1561
1949
  // When progress was made (agent added messages before hitting the
1562
1950
  // limit), incorporate those new messages into ctx.messages so the
@@ -1572,11 +1960,11 @@ export async function runAgentLoopImpl(
1572
1960
  if (updatedHistory.length > preRunHistoryLength) {
1573
1961
  ctx.messages = stripInjectionsForCompaction(updatedHistory);
1574
1962
  try {
1575
- clearPkbSystemReminderMetadataForConversation(ctx.conversationId);
1963
+ clearStrippedInjectionMetadataForConversation(ctx.conversationId);
1576
1964
  } catch (err) {
1577
1965
  rlog.warn(
1578
1966
  { err },
1579
- "Failed to clear pkbSystemReminderBlock metadata after compaction strip (non-fatal)",
1967
+ "Failed to clear stripped-injection metadata after compaction strip (non-fatal)",
1580
1968
  );
1581
1969
  }
1582
1970
  convergenceStripped = true;
@@ -1675,7 +2063,7 @@ export async function runAgentLoopImpl(
1675
2063
  step.compactionResult &&
1676
2064
  step.compactionResult.summaryFailed !== undefined
1677
2065
  ) {
1678
- trackCompactionOutcome(
2066
+ await trackCompactionOutcome(
1679
2067
  ctx,
1680
2068
  step.compactionResult.summaryFailed,
1681
2069
  onEvent,
@@ -1683,47 +2071,7 @@ export async function runAgentLoopImpl(
1683
2071
  }
1684
2072
 
1685
2073
  if (step.compactionResult?.compacted) {
1686
- ctx.contextCompactedMessageCount +=
1687
- step.compactionResult.compactedPersistedMessages;
1688
- ctx.contextCompactedAt = Date.now();
1689
- updateConversationContextWindow(
1690
- ctx.conversationId,
1691
- step.compactionResult.summaryText,
1692
- ctx.contextCompactedMessageCount,
1693
- );
1694
- // Fire auto-analysis on compaction — see forceCompact() for rationale.
1695
- enqueueAutoAnalysisOnCompaction(
1696
- ctx.conversationId,
1697
- ctx.trustContext?.trustClass,
1698
- );
1699
- onEvent({
1700
- type: "context_compacted",
1701
- previousEstimatedInputTokens:
1702
- step.compactionResult.previousEstimatedInputTokens,
1703
- estimatedInputTokens: step.compactionResult.estimatedInputTokens,
1704
- maxInputTokens: step.compactionResult.maxInputTokens,
1705
- thresholdTokens: step.compactionResult.thresholdTokens,
1706
- compactedMessages: step.compactionResult.compactedMessages,
1707
- summaryCalls: step.compactionResult.summaryCalls,
1708
- summaryInputTokens: step.compactionResult.summaryInputTokens,
1709
- summaryOutputTokens: step.compactionResult.summaryOutputTokens,
1710
- summaryModel: step.compactionResult.summaryModel,
1711
- });
1712
- emitUsage(
1713
- ctx,
1714
- step.compactionResult.summaryInputTokens,
1715
- step.compactionResult.summaryOutputTokens,
1716
- step.compactionResult.summaryModel,
1717
- onEvent,
1718
- "context_compactor",
1719
- reqId,
1720
- step.compactionResult.summaryCacheCreationInputTokens ?? 0,
1721
- step.compactionResult.summaryCacheReadInputTokens ?? 0,
1722
- collapseRawResponses(step.compactionResult.summaryRawResponses),
1723
- );
1724
- ctx.graphMemory.onCompacted(
1725
- step.compactionResult.compactedPersistedMessages,
1726
- );
2074
+ applyCompactionResult(ctx, step.compactionResult, onEvent, reqId);
1727
2075
  shouldInjectWorkspace = true;
1728
2076
  reducerCompacted = true;
1729
2077
  }
@@ -1742,6 +2090,7 @@ export async function runAgentLoopImpl(
1742
2090
  ? null
1743
2091
  : injectionOpts.slackChronologicalMessages,
1744
2092
  mode: currentInjectionMode,
2093
+ turnContext: buildPluginTurnContext(ctx, reqId),
1745
2094
  });
1746
2095
  runMessages = injection.messages;
1747
2096
  if (isTrustedActor && currentInjectionMode !== "minimal") {
@@ -1767,6 +2116,8 @@ export async function runAgentLoopImpl(
1767
2116
  reqId,
1768
2117
  onCheckpoint,
1769
2118
  turnCallSite,
2119
+ loopTurnCtx,
2120
+ turnOverrideProfile,
1770
2121
  );
1771
2122
 
1772
2123
  // If the rerun still yields at checkpoint, the turn is still
@@ -1789,11 +2140,11 @@ export async function runAgentLoopImpl(
1789
2140
  if (updatedHistory.length > preRunHistoryLength) {
1790
2141
  ctx.messages = stripInjectionsForCompaction(updatedHistory);
1791
2142
  try {
1792
- clearPkbSystemReminderMetadataForConversation(ctx.conversationId);
2143
+ clearStrippedInjectionMetadataForConversation(ctx.conversationId);
1793
2144
  } catch (err) {
1794
2145
  rlog.warn(
1795
2146
  { err },
1796
- "Failed to clear pkbSystemReminderBlock metadata after compaction strip (non-fatal)",
2147
+ "Failed to clear stripped-injection metadata after compaction strip (non-fatal)",
1797
2148
  );
1798
2149
  }
1799
2150
  convergenceStripped = true;
@@ -1805,231 +2156,83 @@ export async function runAgentLoopImpl(
1805
2156
 
1806
2157
  // All reducer tiers exhausted but provider still rejects —
1807
2158
  // consult the overflow policy for latest-turn compression.
1808
- // Emergency compaction is deferred to the policy-gated paths below
1809
- // so that `request_user_approval` sessions collect consent first.
2159
+ // The policy either auto-compresses the latest turn or falls
2160
+ // through to the final graceful-error fallback below.
1810
2161
  if (state.contextTooLargeDetected) {
1811
2162
  const action = resolveOverflowAction({
1812
2163
  overflowRecovery,
1813
2164
  isInteractive: isInteractiveResolved,
1814
2165
  });
1815
2166
 
1816
- if (action === "request_user_approval") {
1817
- const approval = await requestCompressionApproval(ctx.prompter, {
1818
- signal: abortController.signal,
1819
- });
1820
-
1821
- if (approval.approved) {
1822
- // User approved — force emergency compaction with aggressive settings
1823
- const emergencyCompact =
1824
- await ctx.contextWindowManager.maybeCompact(
1825
- ctx.messages,
1826
- abortController.signal,
1827
- {
2167
+ if (action === "auto_compress_latest_turn") {
2168
+ // Auto-compress without asking — users opt out via the "drop" policy.
2169
+ ctx.emitActivityState(
2170
+ "thinking",
2171
+ "context_compacting",
2172
+ "assistant_turn",
2173
+ reqId,
2174
+ );
2175
+ let emergencyCompact: Awaited<
2176
+ ReturnType<typeof ctx.contextWindowManager.maybeCompact>
2177
+ > | null = null;
2178
+ try {
2179
+ emergencyCompact = (await runPipeline<
2180
+ CompactionArgs,
2181
+ CompactionResult
2182
+ >(
2183
+ "compaction",
2184
+ getMiddlewaresFor("compaction"),
2185
+ (args) =>
2186
+ defaultCompactionTerminal(
2187
+ args,
2188
+ buildPluginTurnContext(ctx, reqId),
2189
+ ),
2190
+ {
2191
+ messages: ctx.messages,
2192
+ signal: abortController.signal,
2193
+ options: {
1828
2194
  lastCompactedAt: ctx.contextCompactedAt ?? undefined,
1829
2195
  force: true,
1830
2196
  minKeepRecentUserTurns: 0,
1831
2197
  targetInputTokensOverride: correctedTarget,
1832
2198
  },
2199
+ },
2200
+ buildPluginTurnContext(ctx, reqId),
2201
+ DEFAULT_TIMEOUTS.compaction,
2202
+ )) as Awaited<
2203
+ ReturnType<typeof ctx.contextWindowManager.maybeCompact>
2204
+ >;
2205
+ } catch (err) {
2206
+ if (err instanceof PluginTimeoutError) {
2207
+ // Emergency compaction timed out. Record the circuit-breaker
2208
+ // failure and fall through to the graceful-error path below
2209
+ // (the unsuccessful-compaction fallback) rather than hard-
2210
+ // failing the turn.
2211
+ rlog.warn(
2212
+ { err, phase: "emergency-compaction" },
2213
+ "Emergency compaction pipeline timed out — continuing with overflow fallback",
1833
2214
  );
1834
- // Only track when the summary LLM actually ran; `force: true`
1835
- // bypasses the cooldown but not the early-return paths.
1836
- if (emergencyCompact.summaryFailed !== undefined) {
1837
- trackCompactionOutcome(
1838
- ctx,
1839
- emergencyCompact.summaryFailed,
1840
- onEvent,
1841
- );
1842
- }
1843
- if (emergencyCompact.compacted) {
1844
- ctx.messages = emergencyCompact.messages;
1845
- reducerCompacted = true;
1846
- ctx.contextCompactedMessageCount +=
1847
- emergencyCompact.compactedPersistedMessages;
1848
- ctx.contextCompactedAt = Date.now();
1849
- updateConversationContextWindow(
1850
- ctx.conversationId,
1851
- emergencyCompact.summaryText,
1852
- ctx.contextCompactedMessageCount,
1853
- );
1854
- // Fire auto-analysis on compaction — see forceCompact() for rationale.
1855
- enqueueAutoAnalysisOnCompaction(
1856
- ctx.conversationId,
1857
- ctx.trustContext?.trustClass,
1858
- );
1859
- onEvent({
1860
- type: "context_compacted",
1861
- previousEstimatedInputTokens:
1862
- emergencyCompact.previousEstimatedInputTokens,
1863
- estimatedInputTokens: emergencyCompact.estimatedInputTokens,
1864
- maxInputTokens: emergencyCompact.maxInputTokens,
1865
- thresholdTokens: emergencyCompact.thresholdTokens,
1866
- compactedMessages: emergencyCompact.compactedMessages,
1867
- summaryCalls: emergencyCompact.summaryCalls,
1868
- summaryInputTokens: emergencyCompact.summaryInputTokens,
1869
- summaryOutputTokens: emergencyCompact.summaryOutputTokens,
1870
- summaryModel: emergencyCompact.summaryModel,
1871
- });
1872
- emitUsage(
1873
- ctx,
1874
- emergencyCompact.summaryInputTokens,
1875
- emergencyCompact.summaryOutputTokens,
1876
- emergencyCompact.summaryModel,
1877
- onEvent,
1878
- "context_compactor",
1879
- reqId,
1880
- emergencyCompact.summaryCacheCreationInputTokens ?? 0,
1881
- emergencyCompact.summaryCacheReadInputTokens ?? 0,
1882
- collapseRawResponses(emergencyCompact.summaryRawResponses),
1883
- );
1884
- ctx.graphMemory.onCompacted(
1885
- emergencyCompact.compactedPersistedMessages,
1886
- );
1887
- shouldInjectWorkspace = true;
1888
- }
1889
-
1890
- // Only re-inject NOW.md when ctx.messages was actually stripped;
1891
- // otherwise the existing block is still present.
1892
- const injection = await applyRuntimeInjections(ctx.messages, {
1893
- ...injectionOpts,
1894
- pkbContext: currentPkbContent,
1895
- nowScratchpad: convergenceStripped ? currentNowContent : null,
1896
- workspaceTopLevelContext: shouldInjectWorkspace
1897
- ? ctx.workspaceTopLevelContext
1898
- : null,
1899
- slackChronologicalMessages: reducerCompacted
1900
- ? null
1901
- : injectionOpts.slackChronologicalMessages,
1902
- mode: currentInjectionMode,
1903
- });
1904
- runMessages = injection.messages;
1905
- if (isTrustedActor && currentInjectionMode !== "minimal") {
1906
- ctx.graphMemory.retrackCachedNodes();
1907
- }
1908
- const emergencyStrip = stripHistoricalWebSearchResults(runMessages);
1909
- if (emergencyStrip.stats.blocksStripped > 0) {
1910
- rlog.info(
1911
- { phase: "emergency_compact", ...emergencyStrip.stats },
1912
- "Converted historical web_search_tool_result blocks to text summaries",
1913
- );
1914
- runMessages = emergencyStrip.messages;
2215
+ await trackCompactionOutcome(ctx, true, onEvent);
2216
+ emergencyCompact = null;
2217
+ } else {
2218
+ throw err;
1915
2219
  }
1916
- preRepairMessages = runMessages;
1917
- preRunHistoryLength = runMessages.length;
1918
- state.contextTooLargeDetected = false;
1919
-
1920
- updatedHistory = await ctx.agentLoop.run(
1921
- runMessages,
1922
- eventHandler,
1923
- abortController.signal,
1924
- reqId,
1925
- onCheckpoint,
1926
- turnCallSite,
1927
- );
1928
- } else {
1929
- // User denied compression — emit a graceful assistant explanation
1930
- // instead of a conversation_error, and end the turn cleanly.
1931
- state.contextTooLargeDetected = false;
1932
- const denyText =
1933
- "The conversation has grown too long for the model to process, " +
1934
- "and compression was declined. Please start a new conversation " +
1935
- "or manually shorten the conversation to continue.";
1936
- const loopChannelMeta = {
1937
- ...provenanceFromTrustContext(ctx.trustContext),
1938
- userMessageChannel: capturedTurnChannelContext.userMessageChannel,
1939
- assistantMessageChannel:
1940
- capturedTurnChannelContext.assistantMessageChannel,
1941
- userMessageInterface:
1942
- capturedTurnInterfaceContext.userMessageInterface,
1943
- assistantMessageInterface:
1944
- capturedTurnInterfaceContext.assistantMessageInterface,
1945
- };
1946
- const denyMessage = createAssistantMessage(denyText);
1947
- await addMessage(
1948
- ctx.conversationId,
1949
- "assistant",
1950
- JSON.stringify(denyMessage.content),
1951
- loopChannelMeta,
1952
- );
1953
- denyCompressionMessage = denyMessage;
1954
- onEvent({
1955
- type: "assistant_text_delta",
1956
- text: denyText,
1957
- conversationId: ctx.conversationId,
1958
- });
1959
- // Prevent the final error fallback from firing
1960
- state.providerErrorUserMessage = null;
1961
2220
  }
1962
- } else if (action === "auto_compress_latest_turn") {
1963
- // Non-interactive — auto-compress without asking
1964
- ctx.emitActivityState(
1965
- "thinking",
1966
- "context_compacting",
1967
- "assistant_turn",
1968
- reqId,
1969
- );
1970
- const emergencyCompact = await ctx.contextWindowManager.maybeCompact(
1971
- ctx.messages,
1972
- abortController.signal,
1973
- {
1974
- lastCompactedAt: ctx.contextCompactedAt ?? undefined,
1975
- force: true,
1976
- minKeepRecentUserTurns: 0,
1977
- targetInputTokensOverride: correctedTarget,
1978
- },
1979
- );
1980
2221
  // Only track when the summary LLM actually ran; `force: true`
1981
2222
  // bypasses the cooldown but not the early-return paths.
1982
- if (emergencyCompact.summaryFailed !== undefined) {
1983
- trackCompactionOutcome(
2223
+ if (
2224
+ emergencyCompact &&
2225
+ emergencyCompact.summaryFailed !== undefined
2226
+ ) {
2227
+ await trackCompactionOutcome(
1984
2228
  ctx,
1985
2229
  emergencyCompact.summaryFailed,
1986
2230
  onEvent,
1987
2231
  );
1988
2232
  }
1989
- if (emergencyCompact.compacted) {
1990
- ctx.messages = emergencyCompact.messages;
2233
+ if (emergencyCompact?.compacted) {
2234
+ applyCompactionResult(ctx, emergencyCompact, onEvent, reqId);
1991
2235
  reducerCompacted = true;
1992
- ctx.contextCompactedMessageCount +=
1993
- emergencyCompact.compactedPersistedMessages;
1994
- ctx.contextCompactedAt = Date.now();
1995
- updateConversationContextWindow(
1996
- ctx.conversationId,
1997
- emergencyCompact.summaryText,
1998
- ctx.contextCompactedMessageCount,
1999
- );
2000
- // Fire auto-analysis on compaction — see forceCompact() for rationale.
2001
- enqueueAutoAnalysisOnCompaction(
2002
- ctx.conversationId,
2003
- ctx.trustContext?.trustClass,
2004
- );
2005
- onEvent({
2006
- type: "context_compacted",
2007
- previousEstimatedInputTokens:
2008
- emergencyCompact.previousEstimatedInputTokens,
2009
- estimatedInputTokens: emergencyCompact.estimatedInputTokens,
2010
- maxInputTokens: emergencyCompact.maxInputTokens,
2011
- thresholdTokens: emergencyCompact.thresholdTokens,
2012
- compactedMessages: emergencyCompact.compactedMessages,
2013
- summaryCalls: emergencyCompact.summaryCalls,
2014
- summaryInputTokens: emergencyCompact.summaryInputTokens,
2015
- summaryOutputTokens: emergencyCompact.summaryOutputTokens,
2016
- summaryModel: emergencyCompact.summaryModel,
2017
- });
2018
- emitUsage(
2019
- ctx,
2020
- emergencyCompact.summaryInputTokens,
2021
- emergencyCompact.summaryOutputTokens,
2022
- emergencyCompact.summaryModel,
2023
- onEvent,
2024
- "context_compactor",
2025
- reqId,
2026
- emergencyCompact.summaryCacheCreationInputTokens ?? 0,
2027
- emergencyCompact.summaryCacheReadInputTokens ?? 0,
2028
- collapseRawResponses(emergencyCompact.summaryRawResponses),
2029
- );
2030
- ctx.graphMemory.onCompacted(
2031
- emergencyCompact.compactedPersistedMessages,
2032
- );
2033
2236
  shouldInjectWorkspace = true;
2034
2237
  }
2035
2238
 
@@ -2046,6 +2249,7 @@ export async function runAgentLoopImpl(
2046
2249
  ? null
2047
2250
  : injectionOpts.slackChronologicalMessages,
2048
2251
  mode: currentInjectionMode,
2252
+ turnContext: buildPluginTurnContext(ctx, reqId),
2049
2253
  });
2050
2254
  runMessages = injection.messages;
2051
2255
  if (isTrustedActor && currentInjectionMode !== "minimal") {
@@ -2070,6 +2274,8 @@ export async function runAgentLoopImpl(
2070
2274
  reqId,
2071
2275
  onCheckpoint,
2072
2276
  turnCallSite,
2277
+ loopTurnCtx,
2278
+ turnOverrideProfile,
2073
2279
  );
2074
2280
  }
2075
2281
  // action === "fail_gracefully" falls through to the final error below
@@ -2134,11 +2340,19 @@ export async function runAgentLoopImpl(
2134
2340
  assistantMessageInterface:
2135
2341
  capturedTurnInterfaceContext.assistantMessageInterface,
2136
2342
  };
2137
- await addMessage(
2138
- ctx.conversationId,
2139
- "user",
2140
- JSON.stringify(toolResultBlocks),
2141
- toolResultMetadata,
2343
+ await runPipeline<PersistArgs, PersistResult>(
2344
+ "persistence",
2345
+ getMiddlewaresFor("persistence"),
2346
+ defaultPersistenceTerminal,
2347
+ {
2348
+ op: "add",
2349
+ conversationId: ctx.conversationId,
2350
+ role: "user",
2351
+ content: JSON.stringify(toolResultBlocks),
2352
+ metadata: toolResultMetadata,
2353
+ },
2354
+ buildPluginTurnContext(ctx, reqId),
2355
+ DEFAULT_TIMEOUTS.persistence,
2142
2356
  );
2143
2357
  state.pendingToolResults.clear();
2144
2358
  }
@@ -2151,10 +2365,6 @@ export async function runAgentLoopImpl(
2151
2365
  return { ...msg, content: cleanedBlocks };
2152
2366
  });
2153
2367
 
2154
- if (denyCompressionMessage) {
2155
- newMessages.push(denyCompressionMessage);
2156
- }
2157
-
2158
2368
  const hasAssistantResponse = newMessages.some(
2159
2369
  (msg) => msg.role === "assistant",
2160
2370
  );
@@ -2176,11 +2386,19 @@ export async function runAgentLoopImpl(
2176
2386
  const errorAssistantMessage = createAssistantMessage(
2177
2387
  state.providerErrorUserMessage,
2178
2388
  );
2179
- await addMessage(
2180
- ctx.conversationId,
2181
- "assistant",
2182
- JSON.stringify(errorAssistantMessage.content),
2183
- errChannelMeta,
2389
+ await runPipeline<PersistArgs, PersistResult>(
2390
+ "persistence",
2391
+ getMiddlewaresFor("persistence"),
2392
+ defaultPersistenceTerminal,
2393
+ {
2394
+ op: "add",
2395
+ conversationId: ctx.conversationId,
2396
+ role: "assistant",
2397
+ content: JSON.stringify(errorAssistantMessage.content),
2398
+ metadata: errChannelMeta,
2399
+ },
2400
+ buildPluginTurnContext(ctx, reqId),
2401
+ DEFAULT_TIMEOUTS.persistence,
2184
2402
  );
2185
2403
  newMessages.push(errorAssistantMessage);
2186
2404
  // Do NOT send assistant_text_delta here — handleProviderError already
@@ -2248,10 +2466,6 @@ export async function runAgentLoopImpl(
2248
2466
  },
2249
2467
  );
2250
2468
 
2251
- void getHookManager().trigger("post-message", {
2252
- conversationId: ctx.conversationId,
2253
- });
2254
-
2255
2469
  const syncLastAssistantMessageToDisk = (): void => {
2256
2470
  if (!state.lastAssistantMessageId) return;
2257
2471
  const convForDisk = getConversation(ctx.conversationId);
@@ -2368,13 +2582,92 @@ export async function runAgentLoopImpl(
2368
2582
  ? { messageId: state.lastAssistantMessageId }
2369
2583
  : {}),
2370
2584
  });
2585
+
2586
+ // Emit a home-feed event for background/scheduled conversation completions.
2587
+ // Scoped to message_complete only (not cancelled/handoff), wrapped in
2588
+ // try-catch so malformed message content can never propagate errors.
2589
+ try {
2590
+ const conv = getConversation(ctx.conversationId);
2591
+ if (
2592
+ conv &&
2593
+ (conv.conversationType === "background" ||
2594
+ conv.conversationType === "scheduled")
2595
+ ) {
2596
+ const lastMsg = state.lastAssistantMessageId
2597
+ ? getMessageById(state.lastAssistantMessageId, ctx.conversationId)
2598
+ : undefined;
2599
+ let summary: string;
2600
+ if (lastMsg) {
2601
+ const parsed: unknown = JSON.parse(lastMsg.content);
2602
+ if (typeof parsed === "string") {
2603
+ summary = parsed.slice(0, 200);
2604
+ } else if (Array.isArray(parsed)) {
2605
+ const textBlock = parsed.find(
2606
+ (b: { type?: string }) => b.type === "text",
2607
+ );
2608
+ summary =
2609
+ typeof textBlock?.text === "string"
2610
+ ? textBlock.text.slice(0, 200)
2611
+ : (conv.title ?? "Background task completed.");
2612
+ } else {
2613
+ summary = conv.title ?? "Background task completed.";
2614
+ }
2615
+ } else {
2616
+ summary = conv.title ?? "Background task completed.";
2617
+ }
2618
+ const feedTitle =
2619
+ conv.title && !isReplaceableTitle(conv.title)
2620
+ ? conv.title
2621
+ : "Background task";
2622
+ const dedupKey = `bg-conv:${ctx.conversationId}`;
2623
+ void emitFeedEvent({
2624
+ source: "assistant",
2625
+ title: feedTitle,
2626
+ summary,
2627
+ dedupKey,
2628
+ conversationId: ctx.conversationId,
2629
+ }).catch((err) => {
2630
+ log.warn(
2631
+ { err, conversationId: ctx.conversationId },
2632
+ "Failed to emit background conversation feed event",
2633
+ );
2634
+ });
2635
+
2636
+ if (isReplaceableTitle(conv.title ?? null)) {
2637
+ void rewriteFeedTitle(feedTitle)
2638
+ .then((prose) => {
2639
+ if (prose && prose !== feedTitle) {
2640
+ return emitFeedEvent({
2641
+ source: "assistant",
2642
+ title: prose,
2643
+ summary,
2644
+ dedupKey,
2645
+ conversationId: ctx.conversationId,
2646
+ });
2647
+ }
2648
+ })
2649
+ .catch((err) => {
2650
+ log.warn(
2651
+ { err, conversationId: ctx.conversationId },
2652
+ "Failed to update feed event with prose title rewrite",
2653
+ );
2654
+ });
2655
+ }
2656
+ }
2657
+ } catch (feedErr) {
2658
+ log.warn(
2659
+ { err: feedErr, conversationId: ctx.conversationId },
2660
+ "Failed to build home-feed event for background conversation",
2661
+ );
2662
+ }
2371
2663
  }
2372
2664
  }
2373
2665
 
2374
2666
  // Second title pass: after 3 completed turns, re-generate the title
2375
2667
  // using the last 3 messages for better context. Only fires when the
2376
- // current title was auto-generated (isAutoTitle = 1).
2377
- if (ctx.turnCount === 2) {
2668
+ // current title was auto-generated (isAutoTitle = 1) and the user
2669
+ // has not opted out via `conversations.skipAutoRetitling`.
2670
+ if (ctx.turnCount === 2 && !getConfig().conversations.skipAutoRetitling) {
2378
2671
  // turnCount is 0-indexed, incremented in finally; 2 = about to become 3rd turn
2379
2672
  queueRegenerateConversationTitle({
2380
2673
  conversationId: ctx.conversationId,
@@ -2427,12 +2720,6 @@ export async function runAgentLoopImpl(
2427
2720
  });
2428
2721
  onEvent({ type: "error", message: classified.userMessage });
2429
2722
  onEvent(buildConversationErrorMessage(ctx.conversationId, classified));
2430
- void getHookManager().trigger("on-error", {
2431
- error: err instanceof Error ? err.name : "Error",
2432
- message,
2433
- stack: err instanceof Error ? err.stack : undefined,
2434
- conversationId: ctx.conversationId,
2435
- });
2436
2723
  }
2437
2724
  } finally {
2438
2725
  if (turnStarted) {
@@ -2481,6 +2768,7 @@ export async function runAgentLoopImpl(
2481
2768
  ctx.currentActiveSurfaceId = undefined;
2482
2769
  ctx.allowedToolNames = undefined;
2483
2770
  ctx.preactivatedSkillIds = undefined;
2771
+ ctx.currentTurnOverrideProfile = undefined;
2484
2772
  // Channel command intents (e.g. Telegram /start) are single-turn metadata.
2485
2773
  // Clear at turn end so they never leak into subsequent unrelated messages.
2486
2774
  ctx.commandIntent = undefined;
@@ -2542,7 +2830,131 @@ function emitUsage(
2542
2830
  );
2543
2831
  }
2544
2832
 
2833
+ /**
2834
+ * Minimal context shape consumed by `applyCompactionResult`. Both
2835
+ * `AgentLoopConversationContext` and `Conversation` satisfy this via structural
2836
+ * typing, so the helper can back both the 5 agent-loop auto-compaction sites
2837
+ * and the single `forceCompact` user-initiated site.
2838
+ */
2839
+ export interface CompactionApplyContext {
2840
+ readonly conversationId: string;
2841
+ messages: Message[];
2842
+ contextCompactedMessageCount: number;
2843
+ contextCompactedAt: number | null;
2844
+ readonly graphMemory: ConversationGraphMemory;
2845
+ readonly provider: Provider;
2846
+ usageStats: UsageStats;
2847
+ trustContext?: TrustContext;
2848
+ }
2849
+
2850
+ /**
2851
+ * Applies a successful `ContextWindowResult` to a conversation: updates the
2852
+ * in-memory message buffer and compaction counters, notifies the graph memory
2853
+ * and conversation-summary store, enqueues auto-analysis, emits the
2854
+ * `context_compacted` event, and records a `context_compactor` usage event.
2855
+ *
2856
+ * The emitted `usage_update` intentionally omits `contextWindow` — the
2857
+ * `context_compacted` event already carries the fresh
2858
+ * `estimatedInputTokens` / `maxInputTokens` and is the single source of
2859
+ * truth for the UI indicator after compaction. Emitting both caused a
2860
+ * redundant SwiftUI invalidation on every compaction.
2861
+ */
2862
+ export function applyCompactionResult(
2863
+ ctx: CompactionApplyContext,
2864
+ result: {
2865
+ messages: Message[];
2866
+ compactedPersistedMessages: number;
2867
+ previousEstimatedInputTokens: number;
2868
+ estimatedInputTokens: number;
2869
+ maxInputTokens: number;
2870
+ thresholdTokens: number;
2871
+ compactedMessages: number;
2872
+ summaryCalls: number;
2873
+ summaryInputTokens: number;
2874
+ summaryOutputTokens: number;
2875
+ summaryModel: string;
2876
+ summaryText: string;
2877
+ summaryCacheCreationInputTokens?: number;
2878
+ summaryCacheReadInputTokens?: number;
2879
+ summaryRawResponses?: unknown[];
2880
+ },
2881
+ onEvent: (msg: ServerMessage) => void,
2882
+ reqId: string | null,
2883
+ ): void {
2884
+ ctx.messages = result.messages;
2885
+ ctx.contextCompactedMessageCount += result.compactedPersistedMessages;
2886
+ ctx.contextCompactedAt = Date.now();
2887
+ ctx.graphMemory.onCompacted(result.compactedPersistedMessages);
2888
+ updateConversationContextWindow(
2889
+ ctx.conversationId,
2890
+ result.summaryText,
2891
+ ctx.contextCompactedMessageCount,
2892
+ );
2893
+ enqueueAutoAnalysisOnCompaction(
2894
+ ctx.conversationId,
2895
+ ctx.trustContext?.trustClass,
2896
+ );
2897
+ const summarySignals = computeSummaryQualitySignals(result.summaryText);
2898
+ onEvent({
2899
+ type: "context_compacted",
2900
+ conversationId: ctx.conversationId,
2901
+ previousEstimatedInputTokens: result.previousEstimatedInputTokens,
2902
+ estimatedInputTokens: result.estimatedInputTokens,
2903
+ maxInputTokens: result.maxInputTokens,
2904
+ thresholdTokens: result.thresholdTokens,
2905
+ compactedMessages: result.compactedMessages,
2906
+ summaryCalls: result.summaryCalls,
2907
+ summaryInputTokens: result.summaryInputTokens,
2908
+ summaryOutputTokens: result.summaryOutputTokens,
2909
+ summaryModel: result.summaryModel,
2910
+ summaryCharCount: summarySignals.charCount,
2911
+ summaryHeaderCount: summarySignals.headerCount,
2912
+ summaryHadMemoryEcho: summarySignals.hadMemoryEcho,
2913
+ });
2914
+ emitUsage(
2915
+ ctx,
2916
+ result.summaryInputTokens,
2917
+ result.summaryOutputTokens,
2918
+ result.summaryModel,
2919
+ onEvent,
2920
+ "context_compactor",
2921
+ reqId,
2922
+ result.summaryCacheCreationInputTokens ?? 0,
2923
+ result.summaryCacheReadInputTokens ?? 0,
2924
+ collapseRawResponses(result.summaryRawResponses),
2925
+ undefined /* providerName */,
2926
+ 1 /* llmCallCount */,
2927
+ );
2928
+ }
2929
+
2545
2930
  function collapseRawResponses(rawResponses?: unknown[]): unknown | undefined {
2546
2931
  if (!rawResponses || rawResponses.length === 0) return undefined;
2547
2932
  return rawResponses.length === 1 ? rawResponses[0] : rawResponses;
2548
2933
  }
2934
+
2935
+ /**
2936
+ * Matches any runtime-injection tag that should never appear inside a
2937
+ * generated summary. If the regex hits, either the compaction strip logic
2938
+ * failed to drop an injected block from the summarizer input, or the
2939
+ * summarizer invented tag-like text on its own — both are quality bugs
2940
+ * worth surfacing via telemetry.
2941
+ */
2942
+ const SUMMARY_MEMORY_ECHO_PATTERN =
2943
+ /<(?:memory|memory_context|memory_image|turn_context|workspace|workspace_top_level|knowledge_base|pkb|system_reminder|now_scratchpad|NOW\.md|active_thread|active_subagents|active_workspace|active_dynamic_page|channel_capabilities|transport_hints|system_notice|non_interactive_context|temporal_context|guardian_context|inbound_actor_context|channel_turn_context|interface_turn_context|channel_command_context|voice_call_control)\b/i;
2944
+
2945
+ /**
2946
+ * Compute light-weight quality signals for a compaction summary. Emitted
2947
+ * on every `context_compacted` event so regressions (short outputs,
2948
+ * header collapse, memory-injection leakage) are visible without having
2949
+ * to read the summary text from the DB.
2950
+ */
2951
+ export function computeSummaryQualitySignals(summaryText: string): {
2952
+ charCount: number;
2953
+ headerCount: number;
2954
+ hadMemoryEcho: boolean;
2955
+ } {
2956
+ const charCount = summaryText.length;
2957
+ const headerCount = (summaryText.match(/^## /gm) ?? []).length;
2958
+ const hadMemoryEcho = SUMMARY_MEMORY_ECHO_PATTERN.test(summaryText);
2959
+ return { charCount, headerCount, hadMemoryEcho };
2960
+ }