@vellumai/assistant 0.8.10 → 0.8.11-staging.1

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 (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -32,15 +32,10 @@ import {
32
32
  } from "../config/llm-resolver.js";
33
33
  import { getConfig } from "../config/loader.js";
34
34
  import type { LLMCallSite } from "../config/schemas/llm.js";
35
- import type { ContextWindowConfig } from "../config/types.js";
36
35
  import {
37
36
  derefToolResultReReads,
38
37
  postTurnTruncateToolResults,
39
38
  } from "../context/post-turn-tool-result-truncation.js";
40
- import {
41
- estimatePromptTokens,
42
- getCalibrationProviderKey,
43
- } from "../context/token-estimator.js";
44
39
  import { writeRelationshipState } from "../home/relationship-state-writer.js";
45
40
  import {
46
41
  clearSentryConversationContext,
@@ -72,17 +67,6 @@ import {
72
67
  import { enqueueMemoryRetrospectiveOnCompaction } from "../memory/memory-retrospective-enqueue.js";
73
68
  import { HOOKS } from "../plugin-api/constants.js";
74
69
  import type { UserPromptSubmitContext } from "../plugin-api/types.js";
75
- import { defaultCompact } from "../plugins/defaults/compaction/compact.js";
76
- import {
77
- createInitialReducerState,
78
- reduceContextOverflow,
79
- type ReducerState,
80
- } from "../plugins/defaults/compaction/context-overflow-reducer.js";
81
- import type { ContextWindowCompactOptions } from "../plugins/defaults/compaction/window-manager.js";
82
- import { deepRepairHistory } from "../plugins/defaults/history-repair/terminal.js";
83
- import userPromptSubmitMemoryRetrieval, {
84
- type MemoryRetrievalHookContext,
85
- } from "../plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.js";
86
70
  import { runHook } from "../plugins/pipeline.js";
87
71
  import type { ContentBlock, Message } from "../providers/types.js";
88
72
  import type { Provider } from "../providers/types.js";
@@ -98,7 +82,6 @@ import { truncate } from "../util/truncate.js";
98
82
  import { getWorkspaceGitService } from "../workspace/git-service.js";
99
83
  import { commitTurnChanges } from "../workspace/turn-commit.js";
100
84
  import { cleanAssistantContent } from "./assistant-attachments.js";
101
- import { resolveOverflowAction } from "./context-overflow-policy.js";
102
85
  import type { Conversation } from "./conversation.js";
103
86
  import {
104
87
  createEventHandlerState,
@@ -118,14 +101,11 @@ import {
118
101
  isUserCancellation,
119
102
  } from "./conversation-error.js";
120
103
  import { raceWithTimeout } from "./conversation-media-retry.js";
121
- import type { InjectionMode } from "./conversation-runtime-assembly.js";
122
104
  import {
123
- applyRuntimeInjections,
124
105
  getSlackCompactionWatermarkForPrefix,
125
106
  loadSlackChronologicalContext,
126
107
  resolveTurnInboundActorContext,
127
108
  type SlackChronologicalContext,
128
- stripInjectionsForCompaction,
129
109
  } from "./conversation-runtime-assembly.js";
130
110
  import { markSurfaceCompleted } from "./conversation-surfaces.js";
131
111
  import { recordUsage } from "./conversation-usage.js";
@@ -138,12 +118,7 @@ import type {
138
118
  SurfaceType,
139
119
  UsageStats,
140
120
  } from "./message-protocol.js";
141
- import { parseActualTokensFromError } from "./parse-actual-tokens-from-error.js";
142
- import {
143
- oversizedImageReplacement,
144
- persistUnsendableImageDowngrades,
145
- } from "./persist-unsendable-image.js";
146
- import { resolveTrustClass, type TrustContext } from "./trust-context.js";
121
+ import type { TrustContext } from "./trust-context.js";
147
122
 
148
123
  const log = getLogger("conversation-agent-loop");
149
124
 
@@ -168,34 +143,6 @@ function formatDiskPressureBlockedMessage(): string {
168
143
  return "Storage is critically low, so background processes are paused and remote messages are ignored until the guardian frees enough space. Remote senders should try again later.";
169
144
  }
170
145
 
171
- // ── Image-recovery helpers ───────────────────────────────────────────
172
-
173
- /**
174
- * True when a message's content holds an image the provider may have rejected
175
- * for being oversized — either a top-level image block (user upload) or one
176
- * nested inside a tool_result's contentBlocks (e.g. a browser screenshot).
177
- */
178
- function messageHasImageBlock(content: ContentBlock[]): boolean {
179
- return content.some(
180
- (b) =>
181
- b.type === "image" ||
182
- (b.type === "tool_result" &&
183
- (b.contentBlocks?.some((cb) => cb.type === "image") ?? false)),
184
- );
185
- }
186
-
187
- /**
188
- * Replace an oversized image with its downscaled form or an unsendable note,
189
- * leaving still-sendable images untouched. Delegates to the shared
190
- * {@link oversizedImageReplacement} so the in-memory recovery and the durable
191
- * persist pass apply the identical provider-cap gate.
192
- */
193
- function recoverImageBlock(
194
- block: Extract<ContentBlock, { type: "image" }>,
195
- ): ContentBlock {
196
- return oversizedImageReplacement(block) ?? block;
197
- }
198
-
199
146
  // ── Plugin pipeline helpers ──────────────────────────────────────────
200
147
 
201
148
  /**
@@ -210,19 +157,6 @@ const FALLBACK_TURN_TRUST: TrustContext = {
210
157
  trustClass: "unknown",
211
158
  };
212
159
 
213
- /**
214
- * Trust class of the actor whose turn is in progress, for the compactor's
215
- * image manifest filter. Prefers the turn-start snapshot
216
- * ({@link Conversation.currentTurnTrustContext}) over the live
217
- * trust context so compaction running in a later tool iteration can't pick up
218
- * a concurrent request's actor.
219
- */
220
- function resolveTurnActorTrustClass(
221
- ctx: Conversation,
222
- ): TrustContext["trustClass"] | undefined {
223
- return (ctx.currentTurnTrustContext ?? ctx.trustContext)?.trustClass;
224
- }
225
-
226
160
  /**
227
161
  * Per-surface entry tracked on the current turn. Inline shape kept stable so
228
162
  * routes and persistence helpers can consume it via a named import instead of
@@ -303,19 +237,21 @@ export async function runAgentLoopImpl(
303
237
  requestId: reqId,
304
238
  });
305
239
  let yieldedForHandoff = false;
306
- let yieldedForBudget = false;
307
- // Whether the most recent agent-loop run produced at least one new assistant
308
- // message — the loop's own forward-progress signal, used by the ordering
309
- // retry gate and the overflow convergence fold.
310
- let lastRunAppendedNewMessages = false;
311
240
  // The messages the most recent agent-loop run appended on top of its base —
312
241
  // the loop's own new-output boundary, persisted as this turn's new messages.
313
242
  let lastRunNewMessages: Message[] = [];
314
- let pendingCheckpointYield: "budget" | "handoff" | null = null;
315
- // Captured when the auto_compress_latest_turn rerun yields at the mid-loop
316
- // budget checkpoint. SSE emission happens immediately at the detection site;
317
- // assistant-row persistence is deferred until after the pendingToolResults
318
- // flush so we don't orphan tool_use/tool_result pairs in the durable history.
243
+ // Terminal context-overflow outcome the agent loop emitted this turn (it
244
+ // drives recovery through the compaction reduction ladder and classifies the
245
+ // exit). The wrapper reads it to persist the matching user-facing notice
246
+ // after the tool-result flush; null when the turn did not end in overflow.
247
+ let overflowTerminalReason:
248
+ | "context_too_large"
249
+ | "budget_yield_unrecovered"
250
+ | null = null;
251
+ // Set when the loop ends the turn as `budget_yield_unrecovered`. SSE emission
252
+ // happens immediately at the detection site; assistant-row persistence is
253
+ // deferred until after the pendingToolResults flush so we don't orphan
254
+ // tool_use/tool_result pairs in the durable history.
319
255
  let budgetYieldClassification: ReturnType<
320
256
  typeof budgetYieldUnrecoveredClassification
321
257
  > | null = null;
@@ -429,34 +365,12 @@ export async function runAgentLoopImpl(
429
365
  refreshCurrentProfileState();
430
366
  return currentEffectiveContextWindow.maxInputTokens;
431
367
  };
432
- const resolveCurrentContextWindowConfig = (): ContextWindowConfig => {
433
- refreshCurrentProfileState();
434
- return currentContextWindowConfig;
435
- };
436
- const resolveCurrentContextBudget = (): {
437
- overflowRecovery: EffectiveContextWindow["overflowRecovery"];
438
- providerMaxTokens: number;
439
- preflightBudget: number;
440
- } => {
441
- refreshCurrentProfileState();
442
- const overflowRecovery = currentEffectiveContextWindow.overflowRecovery;
443
- const providerMaxTokens = currentEffectiveContextWindow.maxInputTokens;
444
- const baseSafetyMargin = overflowRecovery.safetyMarginRatio;
445
- const messageCount = ctx.messages.length;
446
- const safetyMargin =
447
- messageCount > 50 ? Math.max(baseSafetyMargin, 0.15) : baseSafetyMargin;
448
- return {
449
- overflowRecovery,
450
- providerMaxTokens,
451
- preflightBudget: Math.floor(providerMaxTokens * (1 - safetyMargin)),
452
- };
453
- };
454
368
  /**
455
- * The agent loop's window into the orchestrator's current effective
456
- * context window. The loop reads `maxInputTokens` for tool-result
457
- * truncation and `overflowRecovery` for its mid-loop budget gate, applying
458
- * the long-history safety-margin bump itself off its own running history.
459
- * Resolved fresh on each access so a mid-turn profile change is reflected.
369
+ * The agent loop's window into the wrapper's current effective context
370
+ * window. The loop reads `maxInputTokens` for tool-result truncation and
371
+ * `overflowRecovery` for its mid-loop budget gate, applying the long-history
372
+ * safety-margin bump itself off its own running history. Resolved fresh on
373
+ * each access so a mid-turn profile change is reflected.
460
374
  */
461
375
  const resolveContextWindow = (): {
462
376
  maxInputTokens: number;
@@ -801,14 +715,15 @@ export async function runAgentLoopImpl(
801
715
 
802
716
  // Unified `<turn_context>` actor input for this turn (model-facing grounding
803
717
  // metadata; the conversation runtime context remains the source for policy
804
- // gating). Resolved once at turn start and threaded per call site (like
805
- // `modelProfile`) so post-compaction re-injection receives it as an explicit
806
- // hook input rather than re-deriving it from live state that can flip
807
- // mid-turn.
718
+ // gating). Resolved once at turn start and frozen onto the conversation so
719
+ // the post-compaction hook re-emits this same value during in-loop recovery
720
+ // instead of re-resolving against contact/member registry state that may
721
+ // have drifted mid-turn.
808
722
  const actorContext = resolveTurnInboundActorContext(
809
723
  ctx.trustContext,
810
724
  ctx.assistantId,
811
725
  );
726
+ ctx.currentTurnInboundActorContext = actorContext;
812
727
 
813
728
  // Surface long gaps between user messages so the model can acknowledge
814
729
  // the absence naturally. Gated at >12h to avoid noisy injection during
@@ -851,90 +766,55 @@ export async function runAgentLoopImpl(
851
766
  config.llm.activeProfile ??
852
767
  resolveDefaultProfileKey("mainAgent", config.llm);
853
768
  const lastNotified = ctx.lastNotifiedInferenceProfile;
854
- let modelProfileStr: string | null = null;
855
- if (effectiveProfileKey != null && effectiveProfileKey !== lastNotified) {
856
- const profileEntry = config.llm.profiles?.[effectiveProfileKey];
857
- const resolved = resolveCallSiteConfig(turnCallSite, config.llm, {
858
- overrideProfile: turnOverrideProfile ?? undefined,
859
- });
860
- const label = profileEntry?.label ?? effectiveProfileKey;
861
- modelProfileStr = resolved.model ? `${label} (${resolved.model})` : label;
769
+ const modelProfileKey =
770
+ effectiveProfileKey != null && effectiveProfileKey !== lastNotified
771
+ ? effectiveProfileKey
772
+ : null;
773
+ // The key is threaded as plain turn data to the user-prompt-submit and
774
+ // post-compaction hooks, which render the `Label (model)` line from it
775
+ // themselves.
776
+ if (modelProfileKey != null) {
862
777
  // Record the notification for persistence on delivery rather than here:
863
778
  // the model only "learns" the profile once it receives this turn
864
779
  // context, signalled by the first `message_complete`. Persisting inline
865
780
  // would mark the profile notified even if the turn is cancelled or fails
866
781
  // before the model ever sees the notice.
867
- state.pendingNotifiedInferenceProfile = effectiveProfileKey;
782
+ state.pendingNotifiedInferenceProfile = modelProfileKey;
868
783
  }
869
784
 
870
- // Memory retrieval + runtime injection — fetches PKB / NOW.md / memory-graph
871
- // outputs, persists the retrieval's own side effects (injected-block
872
- // metadata, recall log, `memory_recalled` event), and assembles the turn's
873
- // runtime-injection blocks onto the history (persisting those blocks too).
874
- // Runs at the early "prompt submitted, before context assembly" moment so
875
- // its injected output is what the agent loop receives. It is shaped as the
876
- // `user-prompt-submit-temp` hook handler but invoked directly for now,
877
- // separate from the canonical late `user-prompt-submit` hook (history
878
- // repair, title) that fires just before the loop.
879
- // The injection inputs (`isNonInteractive`, `modelProfile`) are resolved
880
- // once at turn start and threaded in so post-compaction re-injection reuses
881
- // the same snapshot rather than live state that can flip mid-turn.
882
- const isTrustedActor = resolveTrustClass(ctx.trustContext) === "guardian";
883
- let currentInjectionMode: InjectionMode = "full";
884
- const memoryCtx: MemoryRetrievalHookContext = {
885
- onEvent,
886
- conversationId: ctx.conversationId,
887
- userMessageId,
888
- logger: rlog,
889
- latestMessages: ctx.messages,
890
- requestId: reqId,
891
- isNonInteractive,
892
- modelProfile: modelProfileStr,
893
- };
894
- await userPromptSubmitMemoryRetrieval(memoryCtx);
895
-
896
- // The hook owns its side effects (injected-block metadata, recall log,
897
- // `memory_recalled` event, and the runtime-injection metadata persist) and
898
- // records the dense/sparse PKB query pair on the graph handle for the
899
- // PKB-reminder injector to read back; the loop reuses the fully injected
900
- // message list downstream.
901
- let runMessages = memoryCtx.latestMessages;
902
-
903
- // user-prompt-submit hook: plugins may transform `runMessages` right
904
- // before the agent loop receives them. Fires once per user turn at the
905
- // primary `agentLoop.run` only — the re-entry / retry calls further down
906
- // in this function do not refire it (they're not new user submissions).
907
- // Plugins may mutate `ctx.latestMessages` in place OR return a new
908
- // context with a fresh array; `runHook` forwards whichever the chain
909
- // settles on. Order is plugin registration order.
910
- //
911
- // Fires BEFORE the agent loop runs so the hook-emitted messages are part
912
- // of the loop's input; the loop then reports its own appended output via
913
- // `AgentLoopRunResult.newMessages`, which is what persistence consumes.
785
+ // user-prompt-submit hook chain. Fires once per user turn at the primary
786
+ // `agentLoop.run` (the re-entry / retry calls further down do not refire it
787
+ // — they're not new user submissions), before the loop runs so the
788
+ // hook-assembled messages are part of its input. Memory retrieval runs
789
+ // first — fetching PKB / NOW.md / memory-graph outputs, persisting its own
790
+ // side effects (injected-block metadata, recall log, `memory_recalled`
791
+ // event), and assembling the turn's runtime-injection blocks onto the
792
+ // history — followed by history repair and title generation, which see the
793
+ // fully injected history. Plugins may mutate `ctx.latestMessages` in place
794
+ // OR return a new context with a fresh array; `runHook` forwards whichever
795
+ // the chain settles on, in plugin registration order. The loop then reports
796
+ // its own appended output via `AgentLoopRunResult.newMessages`, which
797
+ // persistence consumes.
914
798
  const userPromptCtx: UserPromptSubmitContext = {
915
799
  conversationId: ctx.conversationId,
916
800
  userMessageId,
917
801
  requestId: reqId,
918
802
  prompt: options?.titleText ?? content,
919
803
  originalMessages: ctx.messages,
920
- latestMessages: runMessages,
804
+ latestMessages: ctx.messages,
921
805
  logger: rlog,
806
+ modelProfileKey,
807
+ isNonInteractive,
922
808
  };
923
809
  const finalUserPromptCtx = await runHook(
924
810
  HOOKS.USER_PROMPT_SUBMIT,
925
811
  userPromptCtx,
926
812
  );
927
- runMessages = finalUserPromptCtx.latestMessages;
928
-
929
- // Reducer state, tool-token budget, and calibration provider key consumed
930
- // by the post-rejection convergence loop further down. The tool-token
931
- // budget is resolved once per turn (the resolved tool set is stable across
932
- // the turn); the calibration key matches the key recorded by `handleUsage`
933
- // for wrapper providers (OpenRouter routing to Anthropic → key is
934
- // `"anthropic"`).
935
- let reducerState: ReducerState | undefined;
936
- const toolTokenBudget = ctx.agentLoop.getToolTokenBudget(runMessages);
937
- const estimationProviderName = getCalibrationProviderKey(ctx.provider);
813
+ const runMessages = finalUserPromptCtx.latestMessages;
814
+
815
+ // Reset the manager's turn-scoped overflow-recovery ladder at the turn
816
+ // boundary so a new turn starts the ladder fresh from the emergency rung.
817
+ ctx.contextWindowManager.resetOverflowRecovery();
938
818
 
939
819
  const shouldGenerateTitle = isReplaceableTitle(
940
820
  getConversation(ctx.conversationId)?.title ?? null,
@@ -951,10 +831,32 @@ export async function runAgentLoopImpl(
951
831
  turnInterfaceContext: capturedTurnInterfaceContext,
952
832
  applyCompaction: applySuccessfulCompaction,
953
833
  };
954
- const eventHandler = (event: AgentEvent): Promise<void> =>
955
- dispatchAgentEvent(state, deps, event);
834
+ const eventHandler = (event: AgentEvent): Promise<void> => {
835
+ if (
836
+ event.type === "agent_loop_exit" &&
837
+ (event.reason === "context_too_large" ||
838
+ event.reason === "budget_yield_unrecovered")
839
+ ) {
840
+ overflowTerminalReason = event.reason;
841
+ if (event.reason === "budget_yield_unrecovered") {
842
+ // The loop emits this terminal exit inline as it breaks. Stamping the
843
+ // exit reason now would land on the last real LLM call before the
844
+ // wrapper has recorded the synthetic yield row below — and the
845
+ // wrapper then stamps again after that row exists, double-stamping
846
+ // two real rows. Capture the reason here and let the wrapper drive a
847
+ // single stamp via `emitTerminalExit` once the synthetic row is in
848
+ // place, preserving the "latest LLM call carries the exit reason"
849
+ // invariant.
850
+ return Promise.resolve();
851
+ }
852
+ }
853
+ return dispatchAgentEvent(state, deps, event);
854
+ };
956
855
  emitTerminalExit = async (reason: AgentLoopExitReason): Promise<void> => {
957
- await eventHandler({ type: "agent_loop_exit", reason });
856
+ await dispatchAgentEvent(state, deps, {
857
+ type: "agent_loop_exit",
858
+ reason,
859
+ });
958
860
  };
959
861
 
960
862
  const onCheckpoint = async (): Promise<CheckpointDecision> => {
@@ -978,49 +880,41 @@ export async function runAgentLoopImpl(
978
880
  ctx.currentTurnTrustContext ?? ctx.trustContext ?? FALLBACK_TURN_TRUST;
979
881
 
980
882
  /**
981
- * Shared closure: runs the agent loop with the orchestrator's turn
982
- * context and maps the loop's returned checkpoint pause-reason into the
983
- * orchestrator's yield bookkeeping. Returns the updated history so call
984
- * sites consume it exactly as before. Pass `compactInPlace` only for the
985
- * primary run: the loop then runs its budget gate before the first call
986
- * (subsuming the proactive turn-start compaction) and compacts in place
987
- * whenever the gate trips. Reruns omit it, skip the first-call gate, and
988
- * keep yielding for budget.
883
+ * Shared closure: runs the agent loop with the wrapper's turn context and
884
+ * maps the loop's returned checkpoint pause-reason into the wrapper's yield
885
+ * bookkeeping. Returns the updated history so call sites consume it exactly
886
+ * as before. Pass `compactInPlace` only for the primary run: the loop then
887
+ * runs its budget gate before the first call (subsuming the proactive
888
+ * turn-start compaction) and compacts in place whenever the gate trips.
889
+ * Reruns omit it and skip the first-call gate.
989
890
  */
990
891
  const runAgentLoop = async (
991
892
  msgs: Message[],
992
893
  compactInPlace = false,
993
894
  ): Promise<Message[]> => {
994
- const { history, exitReason, appendedNewMessages, newMessages } =
995
- await ctx.agentLoop.run({
996
- messages: msgs,
997
- onEvent: eventHandler,
998
- signal: abortController.signal,
999
- requestId: reqId,
1000
- onCheckpoint,
1001
- callSite: turnCallSite,
1002
- trust: loopTrust,
1003
- overrideProfile: turnOverrideProfile,
1004
- resolveOverrideProfile: resolveCurrentOverrideProfile,
1005
- resolveContextWindow,
1006
- compactInPlace,
1007
- isNonInteractive,
1008
- modelProfile: modelProfileStr,
1009
- actorContext,
1010
- });
1011
- lastRunAppendedNewMessages = appendedNewMessages;
895
+ const { history, exitReason, newMessages } = await ctx.agentLoop.run({
896
+ messages: msgs,
897
+ onEvent: eventHandler,
898
+ signal: abortController.signal,
899
+ requestId: reqId,
900
+ onCheckpoint,
901
+ callSite: turnCallSite,
902
+ trust: loopTrust,
903
+ overrideProfile: turnOverrideProfile,
904
+ resolveOverrideProfile: resolveCurrentOverrideProfile,
905
+ resolveContextWindow,
906
+ compactInPlace,
907
+ isNonInteractive,
908
+ modelProfileKey,
909
+ });
1012
910
  lastRunNewMessages = newMessages;
1013
911
  if (exitReason === "handoff") {
1014
912
  yieldedForHandoff = true;
1015
- pendingCheckpointYield = "handoff";
1016
- } else if (exitReason === "budget") {
1017
- yieldedForBudget = true;
1018
- pendingCheckpointYield = "budget";
1019
913
  }
1020
914
  return history;
1021
915
  };
1022
916
 
1023
- let updatedHistory = await runAgentLoop(runMessages, true);
917
+ const updatedHistory = await runAgentLoop(runMessages, true);
1024
918
 
1025
919
  rlog.info(
1026
920
  { resultMessageCount: updatedHistory.length },
@@ -1029,455 +923,34 @@ export async function runAgentLoopImpl(
1029
923
 
1030
924
  if (yieldedForHandoff) {
1031
925
  await emitTerminalExit?.("checkpoint_handoff");
1032
- pendingCheckpointYield = null;
1033
- }
1034
-
1035
- // The loop compacts in place when its budget gate trips and only yields
1036
- // `exitReason = "budget"` when that inline compaction timed out or
1037
- // exhausted its retry budget (the `reinject` hook has already restored
1038
- // runtime context for the productive case). Escalate to the convergence
1039
- // loop's more aggressive reducer tiers so a half-finished turn doesn't
1040
- // reach the user.
1041
- if (yieldedForBudget && !abortController.signal.aborted) {
1042
- rlog.warn(
1043
- { phase: "mid-loop-compact" },
1044
- "Inline compaction could not get under budget — escalating to convergence loop",
1045
- );
1046
- state.contextTooLargeDetected = true;
1047
- }
1048
-
1049
- // One-shot ordering error retry
1050
- if (state.orderingErrorDetected && !lastRunAppendedNewMessages) {
1051
- rlog.warn(
1052
- { phase: "retry" },
1053
- "Provider ordering error detected, attempting one-shot deep-repair retry",
1054
- );
1055
- // Design note: deep-repair intentionally stays a direct call rather
1056
- // than running through the `user-prompt-submit` hook chain. Deep-repair
1057
- // is a recovery-only path triggered by a provider ordering error — it
1058
- // must be deterministic and unaffected by user hooks that might have
1059
- // caused (or be unable to recover from) the original drift. Plugins can
1060
- // already observe / transform the pre-run repair via the
1061
- // `user-prompt-submit` hook (the default history-repair plugin runs
1062
- // `repairHistory` there); widening that surface to deep-repair is
1063
- // intentionally deferred until there's a concrete plugin-level use case.
1064
- const retryRepair = deepRepairHistory(updatedHistory);
1065
- runMessages = retryRepair.messages;
1066
- state.orderingErrorDetected = false;
1067
- state.deferredOrderingError = null;
1068
-
1069
- updatedHistory = await runAgentLoop(runMessages);
1070
-
1071
- if (state.orderingErrorDetected) {
1072
- rlog.error(
1073
- { phase: "retry" },
1074
- "Deep-repair retry also failed with ordering error. Consider starting a new conversation if this persists.",
1075
- );
1076
- }
1077
- }
1078
-
1079
- // ── Image-dimension overflow recovery ──────────────────────────
1080
- // When the provider rejects because an image block exceeds its pixel
1081
- // or payload cap, recover every oversized image in ctx.messages and
1082
- // retry once. recoverImageBlock downscales an oversized image, or swaps
1083
- // it for a text note when resize is a no-op (e.g. sips unavailable
1084
- // off macOS), while leaving still-sendable images untouched. This covers
1085
- // both top-level image blocks (user uploads) and images nested inside a
1086
- // tool_result's contentBlocks (e.g. a browser screenshot), which is where
1087
- // the rejected block usually lives.
1088
- if (state.imageTooLargeDetected) {
1089
- state.imageTooLargeDetected = false;
1090
- rlog.warn(
1091
- { phase: "image-recovery" },
1092
- "Image too large — recovering oversized image blocks and retrying",
1093
- );
1094
- ctx.messages = ctx.messages.map((msg) => {
1095
- if (!Array.isArray(msg.content)) return msg;
1096
- if (!messageHasImageBlock(msg.content)) return msg;
1097
- return {
1098
- ...msg,
1099
- content: msg.content.flatMap((b): ContentBlock[] => {
1100
- if (b.type === "image") return [recoverImageBlock(b)];
1101
- // Images returned by a tool (e.g. browser_screenshot) live in
1102
- // the tool_result's contentBlocks, not as top-level blocks.
1103
- // Recover them in place so the tool_use/tool_result pairing
1104
- // stays intact rather than dropping the whole tool_result.
1105
- if (b.type === "tool_result" && b.contentBlocks?.length) {
1106
- return [
1107
- {
1108
- ...b,
1109
- contentBlocks: b.contentBlocks.map((cb) =>
1110
- cb.type === "image" ? recoverImageBlock(cb) : cb,
1111
- ),
1112
- },
1113
- ];
1114
- }
1115
- return [b];
1116
- }),
1117
- };
1118
- });
1119
- // The transform above only mutates ctx.messages for the current retry.
1120
- // Persist the downgrade for images that can never be sent so the rejected
1121
- // upload doesn't rehydrate from the DB and resurface on later turns. This
1122
- // is cleanup for future turns, so a persistence failure must never abort
1123
- // the retry that is about to run — log it and continue.
1124
- try {
1125
- const rewritten = persistUnsendableImageDowngrades(ctx.conversationId);
1126
- if (rewritten > 0) {
1127
- rlog.info(
1128
- { phase: "image-recovery", rewritten },
1129
- "Persisted unsendable-image downgrades so they cannot resurface",
1130
- );
1131
- }
1132
- } catch (err) {
1133
- rlog.warn(
1134
- { phase: "image-recovery", err },
1135
- "Failed to persist unsendable-image downgrade; continuing with in-memory recovery",
1136
- );
1137
- }
1138
- runMessages = ctx.messages;
1139
- updatedHistory = await runAgentLoop(runMessages);
1140
- if (state.imageTooLargeDetected) {
1141
- rlog.error(
1142
- { phase: "image-recovery" },
1143
- "Image-recovery retry also failed — surfacing error to user",
1144
- );
1145
- const classified = classifyConversationError(
1146
- new Error("Image dimensions too large"),
1147
- { phase: "agent_loop" },
1148
- );
1149
- deps.onEvent(
1150
- buildConversationErrorMessage(deps.ctx.conversationId, classified),
1151
- );
1152
- state.providerErrorUserMessage = classified.userMessage;
1153
- state.imageTooLargeDetected = false;
1154
- }
1155
- }
1156
-
1157
- // ── Bounded context overflow convergence loop ──────────────────
1158
- // When the provider rejects with context-too-large, iterate through
1159
- // reducer tiers (forced compaction, tool-result truncation, media
1160
- // stubbing, injection downgrade).
1161
- //
1162
- // When progress was made (agent added messages before hitting the
1163
- // limit), incorporate those new messages into ctx.messages so the
1164
- // convergence loop operates on the full (larger) history.
1165
- if (state.contextTooLargeDetected) {
1166
- if (lastRunAppendedNewMessages) {
1167
- ctx.messages = stripInjectionsForCompaction(updatedHistory);
1168
- markHistoryStrippedBestEffort(ctx.conversationId);
1169
- }
1170
- if (!reducerState) {
1171
- reducerState = createInitialReducerState();
1172
- }
1173
-
1174
- // When the provider reveals the actual token count in its error
1175
- // message (e.g. "242201 tokens > 200000"), use it to correct the
1176
- // compaction target. The estimator may significantly underestimate
1177
- // (e.g. estimated 185k but actual was 242k), so using the
1178
- // uncorrected preflightBudget would still be too high. Passes the raw
1179
- // error so ContextOverflowError.actualTokens can short-circuit the
1180
- // string-regex path for proxy-rewrapped untyped errors.
1181
- const actualTokens = parseActualTokensFromError(
1182
- state.contextTooLargeError,
1183
- );
1184
- const estimatedTokensAtOverflow = estimatePromptTokens(
1185
- ctx.messages,
1186
- ctx.systemPrompt,
1187
- {
1188
- providerName: estimationProviderName,
1189
- toolTokenBudget,
1190
- },
1191
- );
1192
- const convergenceBudget = resolveCurrentContextBudget();
1193
- let correctedTarget = convergenceBudget.preflightBudget;
1194
- if (actualTokens && estimatedTokensAtOverflow > 0) {
1195
- const estimationErrorRatio = actualTokens / estimatedTokensAtOverflow;
1196
- if (estimationErrorRatio > 1.0) {
1197
- correctedTarget = Math.floor(
1198
- convergenceBudget.preflightBudget / estimationErrorRatio,
1199
- );
1200
- rlog.warn(
1201
- {
1202
- phase: "convergence",
1203
- actualTokens,
1204
- estimatedTokens: estimatedTokensAtOverflow,
1205
- estimationErrorRatio: estimationErrorRatio.toFixed(2),
1206
- preflightBudget: convergenceBudget.preflightBudget,
1207
- correctedTarget,
1208
- },
1209
- "Adjusting compaction target based on observed estimation error",
1210
- );
1211
- }
1212
- }
1213
-
1214
- // ── Emergency mid-turn compaction ────────────────────────────
1215
- // Before entering the reducer tier loop, attempt a targeted
1216
- // emergency compaction: summarize everything before the last
1217
- // tool_use + tool_result pair and let the agent continue with
1218
- // [summary, last_tool_call, last_tool_result]. This preserves
1219
- // the agent's most recent action context while aggressively
1220
- // compressing history. Falls through to reducer tiers on failure.
1221
- {
1222
- try {
1223
- const emergencyResult =
1224
- await ctx.contextWindowManager.emergencyCompact(
1225
- ctx.messages,
1226
- {
1227
- previousEstimatedInputTokens: estimatedTokensAtOverflow,
1228
- overrideProfile: resolveCurrentOverrideProfile() ?? null,
1229
- },
1230
- abortController.signal,
1231
- );
1232
- if (emergencyResult.compacted) {
1233
- rlog.info(
1234
- {
1235
- phase: "convergence",
1236
- compactedMessages: emergencyResult.compactedMessages,
1237
- summaryChars: emergencyResult.summaryText.length,
1238
- },
1239
- "Emergency mid-turn compaction succeeded — bypassing reducer tiers",
1240
- );
1241
- if (emergencyResult.summaryFailed !== undefined) {
1242
- await ctx.agentLoop.compactionCircuit.recordOutcome(
1243
- emergencyResult.summaryFailed,
1244
- onEvent,
1245
- );
1246
- }
1247
- if (emergencyResult.compacted) {
1248
- await applySuccessfulCompaction(emergencyResult, ctx.messages);
1249
- }
1250
- // Clear the overflow flag and re-run the agent loop with
1251
- // the compacted context.
1252
- state.contextTooLargeDetected = false;
1253
- }
1254
- } catch (err) {
1255
- rlog.warn(
1256
- { phase: "convergence", err },
1257
- "Emergency mid-turn compaction failed; continuing to reducer tiers",
1258
- );
1259
- }
1260
- // If emergency compaction failed, fall through to reducer tiers.
1261
- }
1262
-
1263
- let convergenceAttempts = 0;
1264
- const maxAttempts = convergenceBudget.overflowRecovery.maxAttempts;
1265
-
1266
- while (
1267
- state.contextTooLargeDetected &&
1268
- convergenceAttempts < maxAttempts &&
1269
- !reducerState.exhausted
1270
- ) {
1271
- convergenceAttempts++;
1272
- rlog.warn(
1273
- {
1274
- phase: "convergence",
1275
- attempt: convergenceAttempts,
1276
- appliedTiers: reducerState.appliedTiers,
1277
- },
1278
- "Context too large — applying next reducer tier",
1279
- );
1280
-
1281
- ctx.emitActivityState("thinking", "context_compacting", {
1282
- requestId: reqId,
1283
- });
1284
- const convergenceCompactionBasis = ctx.messages;
1285
- const step = await reduceContextOverflow(
1286
- convergenceCompactionBasis,
1287
- {
1288
- providerName: estimationProviderName,
1289
- systemPrompt: ctx.systemPrompt,
1290
- contextWindow: resolveCurrentContextWindowConfig(),
1291
- targetTokens: correctedTarget,
1292
- toolTokenBudget,
1293
- },
1294
- reducerState,
1295
- (msgs, signal, opts) =>
1296
- defaultCompact({
1297
- conversationId: ctx.conversationId,
1298
- messages: msgs,
1299
- signal,
1300
- ...((opts ?? {}) as ContextWindowCompactOptions),
1301
- overrideProfile: resolveCurrentOverrideProfile() ?? null,
1302
- actorTrustClass: resolveTurnActorTrustClass(ctx),
1303
- }),
1304
- abortController.signal,
1305
- );
1306
-
1307
- reducerState = step.state;
1308
- ctx.messages = step.messages;
1309
- currentInjectionMode = step.state.injectionMode;
1310
-
1311
- // See the preflight reducer call above for rationale. Only track when
1312
- // the summary LLM actually ran — `summaryFailed === undefined`
1313
- // indicates the reducer's forced compaction took an early-return path
1314
- // without calling the summary LLM.
1315
- if (
1316
- step.compactionResult &&
1317
- step.compactionResult.summaryFailed !== undefined
1318
- ) {
1319
- await ctx.agentLoop.compactionCircuit.recordOutcome(
1320
- step.compactionResult.summaryFailed,
1321
- onEvent,
1322
- );
1323
- }
1324
-
1325
- if (step.compactionResult?.compacted) {
1326
- await applySuccessfulCompaction(
1327
- step.compactionResult,
1328
- convergenceCompactionBasis,
1329
- );
1330
- }
1331
-
1332
- // Only re-inject the memory-static block when ctx.messages was
1333
- // actually stripped; otherwise the existing block is still present and
1334
- // re-injecting would duplicate it. (The `<knowledge_base>` and NOW.md
1335
- // blocks self-gate inside their injectors on whether they are already
1336
- // present in `ctx.messages`.)
1337
- const injection = await applyRuntimeInjections(ctx.messages, {
1338
- isNonInteractive,
1339
- modelProfile: modelProfileStr,
1340
- actorContext,
1341
- mode: currentInjectionMode,
1342
- requestId: reqId,
1343
- conversationId: ctx.conversationId,
1344
- });
1345
- runMessages = injection.messages;
1346
- if (isTrustedActor && currentInjectionMode !== "minimal") {
1347
- ctx.graphMemory.retrackCachedNodes();
1348
- }
1349
- state.contextTooLargeDetected = false;
1350
- yieldedForBudget = false;
1351
-
1352
- updatedHistory = await runAgentLoop(runMessages);
1353
-
1354
- // If the rerun still yields at checkpoint, the turn is still
1355
- // incomplete — continue reducing through the remaining tiers
1356
- // instead of silently dropping the incomplete state.
1357
- if (yieldedForBudget && !abortController.signal.aborted) {
1358
- rlog.warn(
1359
- {
1360
- phase: "convergence",
1361
- attempt: convergenceAttempts,
1362
- appliedTiers: reducerState.appliedTiers,
1363
- },
1364
- "Post-convergence rerun still yielded at checkpoint — continuing reduction",
1365
- );
1366
- state.contextTooLargeDetected = true;
1367
-
1368
- // Fold rerun progress into ctx.messages so the next reducer
1369
- // tier operates on up-to-date history instead of stale
1370
- // pre-rerun messages.
1371
- if (lastRunAppendedNewMessages) {
1372
- ctx.messages = stripInjectionsForCompaction(updatedHistory);
1373
- markHistoryStrippedBestEffort(ctx.conversationId);
1374
- }
1375
- }
1376
- }
1377
-
1378
- // All reducer tiers exhausted but provider still rejects —
1379
- // consult the overflow policy for latest-turn compression.
1380
- // The policy either auto-compresses the latest turn or falls
1381
- // through to the final graceful-error fallback below.
1382
- if (state.contextTooLargeDetected) {
1383
- const action = resolveOverflowAction({
1384
- overflowRecovery: convergenceBudget.overflowRecovery,
1385
- isInteractive: isInteractiveResolved,
1386
- });
1387
-
1388
- if (action === "auto_compress_latest_turn") {
1389
- // Auto-compress without asking — users opt out via the "drop" policy.
1390
- ctx.emitActivityState("thinking", "context_compacting", {
1391
- requestId: reqId,
1392
- });
1393
- const emergencyCompact = await defaultCompact({
1394
- conversationId: ctx.conversationId,
1395
- messages: ctx.messages,
1396
- signal: abortController.signal,
1397
- force: true,
1398
- minKeepRecentUserTurns: 0,
1399
- overrideProfile: resolveCurrentOverrideProfile() ?? null,
1400
- });
1401
- // Only track when the summary LLM actually ran; `force: true`
1402
- // bypasses the auto-threshold gate but not the early-return paths.
1403
- if (emergencyCompact.summaryFailed !== undefined) {
1404
- await ctx.agentLoop.compactionCircuit.recordOutcome(
1405
- emergencyCompact.summaryFailed,
1406
- onEvent,
1407
- );
1408
- }
1409
- if (emergencyCompact.compacted) {
1410
- await applySuccessfulCompaction(emergencyCompact, ctx.messages);
1411
- }
1412
-
1413
- // Only re-inject the memory-static block when ctx.messages was
1414
- // actually stripped; otherwise the existing block is still present.
1415
- // (The `<knowledge_base>`, NOW.md, and v2 static `<info>` blocks
1416
- // self-gate inside their injectors on whether they are already
1417
- // present in `ctx.messages`.)
1418
- const injection = await applyRuntimeInjections(ctx.messages, {
1419
- isNonInteractive,
1420
- modelProfile: modelProfileStr,
1421
- actorContext,
1422
- mode: currentInjectionMode,
1423
- requestId: reqId,
1424
- conversationId: ctx.conversationId,
1425
- });
1426
- runMessages = injection.messages;
1427
- if (isTrustedActor && currentInjectionMode !== "minimal") {
1428
- ctx.graphMemory.retrackCachedNodes();
1429
- }
1430
- state.contextTooLargeDetected = false;
1431
-
1432
- updatedHistory = await runAgentLoop(runMessages);
1433
- }
1434
- // action === "fail_gracefully" falls through to the final error below
1435
- }
1436
-
1437
- // Final fallback: all recovery paths exhausted
1438
- if (state.contextTooLargeDetected) {
1439
- const classified = classifyConversationError(
1440
- new Error("context_length_exceeded"),
1441
- { phase: "agent_loop" },
1442
- );
1443
- await emitTerminalExit?.("context_too_large");
1444
- pendingCheckpointYield = null;
1445
- onEvent(buildConversationErrorMessage(ctx.conversationId, classified));
1446
- } else if (yieldedForBudget && !abortController.signal.aborted) {
1447
- // The auto_compress_latest_turn rerun (action === "auto_compress_latest_turn"
1448
- // above) reset `contextTooLargeDetected` to false before its final
1449
- // `agentLoop.run`, so the context-too-large branch above won't fire
1450
- // even when that rerun yields at the mid-loop budget checkpoint with
1451
- // no further recovery layer to re-enter. Without surfacing this here,
1452
- // the turn terminates silently — the inspector sees `agent_loop_exit_reason
1453
- // = NULL` and the user sees no message at all (just a "ghost" turn).
1454
- //
1455
- // Unlike provider-error persistence at L3091 — which only fires when
1456
- // the loop produced NO assistant output — budget_yield_unrecovered
1457
- // typically yields AFTER one or more successful tool-use iterations,
1458
- // so `hasAssistantResponse` is true and that path would skip us. We
1459
- // capture the classification here so the live SSE event fires
1460
- // immediately, and persist a dedicated notice row below — after the
1461
- // pendingToolResults flush — so the transcript reads as: tool-use →
1462
- // tool results → "I couldn't fit the next step…" notice. Persisting
1463
- // earlier would orphan an assistant(tool_use) from its user(tool_result),
1464
- // breaking provider adjacency on replay.
1465
- budgetYieldClassification = budgetYieldUnrecoveredClassification();
1466
- onEvent(
1467
- buildConversationErrorMessage(
1468
- ctx.conversationId,
1469
- budgetYieldClassification,
1470
- ),
1471
- );
1472
- }
1473
926
  }
1474
927
 
1475
- if (state.deferredOrderingError) {
928
+ // ── Context-overflow terminal notice ───────────────────────────
929
+ // The agent loop drives overflow recovery through the compaction plugin's
930
+ // reduction ladder and, when the ladder is spent and the provider still
931
+ // rejects, emits the terminal exit (`context_too_large` or
932
+ // `budget_yield_unrecovered`) itself. The wrapper only renders the matching
933
+ // user-facing notice. `budget_yield_unrecovered` defers its durable row to
934
+ // after the tool-result flush below (so the transcript reads tool-use →
935
+ // tool-results → notice), so it captures the classification here and emits
936
+ // the live SSE event; the durable write happens further down.
937
+ if (overflowTerminalReason === "context_too_large") {
1476
938
  const classified = classifyConversationError(
1477
- new Error(state.deferredOrderingError),
939
+ new Error("context_length_exceeded"),
1478
940
  { phase: "agent_loop" },
1479
941
  );
1480
942
  onEvent(buildConversationErrorMessage(ctx.conversationId, classified));
943
+ } else if (
944
+ overflowTerminalReason === "budget_yield_unrecovered" &&
945
+ !abortController.signal.aborted
946
+ ) {
947
+ budgetYieldClassification = budgetYieldUnrecoveredClassification();
948
+ onEvent(
949
+ buildConversationErrorMessage(
950
+ ctx.conversationId,
951
+ budgetYieldClassification,
952
+ ),
953
+ );
1481
954
  }
1482
955
 
1483
956
  // Flush remaining tool results. On a normal turn these drain at the next
@@ -1803,10 +1276,6 @@ export async function runAgentLoopImpl(
1803
1276
 
1804
1277
  // Re-check: the user may have cancelled during attachment resolution
1805
1278
  if (abortController.signal.aborted) {
1806
- if (pendingCheckpointYield === "budget") {
1807
- await emitTerminalExit?.("aborted_after_checkpoint");
1808
- pendingCheckpointYield = null;
1809
- }
1810
1279
  ctx.emitActivityState("idle", "generation_cancelled", {
1811
1280
  anchor: "global",
1812
1281
  requestId: reqId,
@@ -1885,10 +1354,6 @@ export async function runAgentLoopImpl(
1885
1354
  aborted: abortController.signal.aborted,
1886
1355
  };
1887
1356
  if (isUserCancellation(err, errorCtx)) {
1888
- if (pendingCheckpointYield === "budget") {
1889
- await emitTerminalExit?.("aborted_after_checkpoint");
1890
- pendingCheckpointYield = null;
1891
- }
1892
1357
  ctx.emitActivityState("idle", "generation_cancelled", {
1893
1358
  anchor: "global",
1894
1359
  requestId: reqId,