mixdog 0.9.3 → 0.9.4

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 (329) hide show
  1. package/package.json +7 -3
  2. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  3. package/scripts/bench/lead-review-tasks.json +20 -0
  4. package/scripts/bench/r4-mixed-tasks.json +20 -0
  5. package/scripts/bench/review-tasks.json +20 -0
  6. package/scripts/bench/round-codex.json +114 -0
  7. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  8. package/scripts/bench/round-mixdog-lead.json +269 -0
  9. package/scripts/bench/round-mixdog.json +126 -0
  10. package/scripts/bench/round-r10-bigsample.json +679 -0
  11. package/scripts/bench/round-r11-codexalign.json +257 -0
  12. package/scripts/bench/round-r4-codex.json +114 -0
  13. package/scripts/bench/round-r4-mixed.json +225 -0
  14. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  15. package/scripts/bench/round-r6-codex.json +114 -0
  16. package/scripts/bench/round-r6-solo.json +257 -0
  17. package/scripts/bench/round-r7-full.json +254 -0
  18. package/scripts/bench/round-r8-fulldefault.json +255 -0
  19. package/scripts/bench-run.mjs +215 -29
  20. package/scripts/freevar-smoke.mjs +95 -0
  21. package/scripts/internal-comms-bench.mjs +1 -0
  22. package/scripts/internal-comms-smoke.mjs +10 -9
  23. package/scripts/mouse-probe.mjs +45 -0
  24. package/scripts/output-style-bench.mjs +13 -6
  25. package/scripts/output-style-smoke.mjs +4 -4
  26. package/scripts/provider-toolcall-test.mjs +7 -3
  27. package/scripts/recall-usecase-cases.json +18 -0
  28. package/scripts/recall-usecase-probe.json +6 -0
  29. package/scripts/session-bench.mjs +152 -6
  30. package/scripts/tool-smoke.mjs +23 -63
  31. package/scripts/tui-render-smoke.mjs +90 -0
  32. package/scripts/webhook-smoke.mjs +208 -0
  33. package/src/agents/debugger/AGENT.md +4 -1
  34. package/src/agents/heavy-worker/AGENT.md +6 -5
  35. package/src/agents/maintainer/AGENT.md +4 -0
  36. package/src/agents/reviewer/AGENT.md +2 -1
  37. package/src/agents/worker/AGENT.md +8 -4
  38. package/src/lib/rules-builder.cjs +4 -0
  39. package/src/mixdog-session-runtime.mjs +632 -2042
  40. package/src/output-styles/default.md +34 -9
  41. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  42. package/src/output-styles/minimal.md +4 -1
  43. package/src/output-styles/simple.md +22 -7
  44. package/src/rules/agent/00-common.md +2 -0
  45. package/src/rules/lead/lead-brief.md +12 -0
  46. package/src/rules/lead/lead-tool.md +0 -11
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  49. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  50. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  51. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  52. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  53. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  54. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  55. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  56. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  58. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  59. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  60. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  62. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  63. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  65. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  66. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  67. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
  76. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
  77. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  80. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  82. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  83. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  84. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  85. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  87. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  88. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  89. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  90. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  91. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  93. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  94. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  95. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  97. package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
  98. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  99. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  100. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  101. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  102. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  103. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  104. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  105. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  106. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  109. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
  110. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  118. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  119. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  120. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  124. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  125. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  126. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  128. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  130. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  131. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  132. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  133. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  134. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  136. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  137. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  138. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  139. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  140. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  141. package/src/runtime/channels/backends/discord.mjs +12 -292
  142. package/src/runtime/channels/index.mjs +229 -663
  143. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  144. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  145. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  146. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  147. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  148. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  149. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  150. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  151. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  152. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  153. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  155. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  156. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  157. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  158. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  159. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  160. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  161. package/src/runtime/channels/lib/webhook.mjs +36 -570
  162. package/src/runtime/channels/tool-defs.mjs +11 -130
  163. package/src/runtime/memory/index.mjs +201 -1948
  164. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  165. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  166. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  167. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  168. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  169. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  170. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  171. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  172. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  173. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  174. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  175. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  177. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  178. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  179. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  180. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  181. package/src/runtime/memory/tool-defs.mjs +5 -13
  182. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  183. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  184. package/src/runtime/search/lib/web-tools.mjs +24 -602
  185. package/src/runtime/shared/atomic-file.mjs +26 -1
  186. package/src/runtime/shared/launcher-control.mjs +2 -2
  187. package/src/runtime/shared/tool-primitives.mjs +308 -0
  188. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  189. package/src/runtime/shared/tool-surface.mjs +80 -898
  190. package/src/runtime/shared/transcript-writer.mjs +23 -0
  191. package/src/runtime/shared/update-checker.mjs +7 -4
  192. package/src/session-runtime/config-helpers.mjs +84 -2
  193. package/src/session-runtime/config-lifecycle.mjs +232 -0
  194. package/src/session-runtime/cwd-plugins.mjs +226 -0
  195. package/src/session-runtime/mcp-glue.mjs +177 -0
  196. package/src/session-runtime/model-recency.mjs +111 -0
  197. package/src/session-runtime/native-search.mjs +247 -0
  198. package/src/session-runtime/output-styles.mjs +11 -9
  199. package/src/session-runtime/prewarm.mjs +142 -0
  200. package/src/session-runtime/provider-models.mjs +278 -0
  201. package/src/session-runtime/provider-usage.mjs +120 -0
  202. package/src/session-runtime/quick-model-rows.mjs +170 -0
  203. package/src/session-runtime/quick-search-models.mjs +46 -0
  204. package/src/session-runtime/session-hooks.mjs +93 -0
  205. package/src/session-runtime/settings-api.mjs +319 -0
  206. package/src/session-runtime/tool-catalog.mjs +29 -29
  207. package/src/session-runtime/tool-defs.mjs +84 -0
  208. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  209. package/src/standalone/agent-tool/helpers.mjs +237 -0
  210. package/src/standalone/agent-tool/notify.mjs +107 -0
  211. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  212. package/src/standalone/agent-tool/render.mjs +152 -0
  213. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  214. package/src/standalone/agent-tool.mjs +110 -671
  215. package/src/standalone/channel-worker.mjs +4 -7
  216. package/src/standalone/explore-tool.mjs +30 -9
  217. package/src/standalone/hook-bus/config.mjs +207 -0
  218. package/src/standalone/hook-bus/constants.mjs +90 -0
  219. package/src/standalone/hook-bus/handlers.mjs +481 -0
  220. package/src/standalone/hook-bus/payload.mjs +31 -0
  221. package/src/standalone/hook-bus/rules.mjs +77 -0
  222. package/src/standalone/hook-bus.mjs +77 -870
  223. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  224. package/src/standalone/opencode-go-login.mjs +5 -1
  225. package/src/standalone/provider-admin.mjs +1 -16
  226. package/src/standalone/usage-dashboard.mjs +3 -1
  227. package/src/tui/App.jsx +945 -8094
  228. package/src/tui/app/app-format.mjs +206 -0
  229. package/src/tui/app/channel-pickers.mjs +510 -0
  230. package/src/tui/app/clipboard.mjs +67 -0
  231. package/src/tui/app/core-memory-picker.mjs +210 -0
  232. package/src/tui/app/extension-pickers.mjs +506 -0
  233. package/src/tui/app/input-parsers.mjs +193 -0
  234. package/src/tui/app/maintenance-pickers.mjs +324 -0
  235. package/src/tui/app/model-options.mjs +330 -0
  236. package/src/tui/app/model-picker.mjs +365 -0
  237. package/src/tui/app/onboarding-steps.mjs +400 -0
  238. package/src/tui/app/project-picker.mjs +247 -0
  239. package/src/tui/app/provider-setup-picker.mjs +580 -0
  240. package/src/tui/app/resume-picker.mjs +55 -0
  241. package/src/tui/app/route-pickers.mjs +419 -0
  242. package/src/tui/app/settings-picker.mjs +490 -0
  243. package/src/tui/app/slash-commands.mjs +101 -0
  244. package/src/tui/app/slash-dispatch.mjs +427 -0
  245. package/src/tui/app/text-layout.mjs +46 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  247. package/src/tui/app/transcript-window.mjs +671 -0
  248. package/src/tui/app/use-mouse-input.mjs +460 -0
  249. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  250. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  251. package/src/tui/app/use-transcript-window.mjs +589 -0
  252. package/src/tui/components/ConfirmBar.jsx +1 -1
  253. package/src/tui/components/Picker.jsx +32 -4
  254. package/src/tui/components/PromptInput.jsx +23 -101
  255. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  256. package/src/tui/components/StatusLine.jsx +63 -12
  257. package/src/tui/components/TextEntryPanel.jsx +11 -0
  258. package/src/tui/components/ToolExecution.jsx +52 -594
  259. package/src/tui/components/TranscriptItem.jsx +105 -0
  260. package/src/tui/components/UsagePanel.jsx +18 -4
  261. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  262. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  263. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  264. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  265. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  266. package/src/tui/display-width.mjs +20 -3
  267. package/src/tui/dist/index.mjs +19652 -18630
  268. package/src/tui/engine/agent-job-feed.mjs +133 -0
  269. package/src/tui/engine/notification-plan.mjs +76 -0
  270. package/src/tui/engine/render-timing.mjs +17 -0
  271. package/src/tui/engine/tool-approval.mjs +94 -0
  272. package/src/tui/engine/tool-card-results.mjs +234 -0
  273. package/src/tui/engine/tool-result-status.mjs +135 -0
  274. package/src/tui/engine.mjs +122 -562
  275. package/src/tui/figures.mjs +5 -0
  276. package/src/tui/index.jsx +105 -0
  277. package/src/tui/input-editing.mjs +2 -2
  278. package/src/tui/markdown/format-token.mjs +4 -1
  279. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  280. package/src/tui/theme.mjs +6 -0
  281. package/src/ui/statusline-agents.mjs +213 -0
  282. package/src/ui/statusline-format.mjs +146 -0
  283. package/src/ui/statusline-segments.mjs +148 -0
  284. package/src/ui/statusline.mjs +67 -501
  285. package/src/ui/tool-card.mjs +0 -1
  286. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  287. package/src/workflows/default/WORKFLOW.md +1 -1
  288. package/src/workflows/sequential/WORKFLOW.md +1 -1
  289. package/vendor/ink/build/display-width.js +19 -3
  290. package/vendor/ink/build/ink.js +103 -6
  291. package/vendor/ink/build/log-update.js +17 -3
  292. package/vendor/ink/build/wrap-text.js +125 -0
  293. package/scripts/_test-folder-dialog.mjs +0 -30
  294. package/scripts/fix-brief-fn.mjs +0 -35
  295. package/scripts/fix-format-tool-surface.mjs +0 -24
  296. package/scripts/fix-tool-exec-visible.mjs +0 -42
  297. package/scripts/patch-agent-brief.mjs +0 -48
  298. package/scripts/patch-app.mjs +0 -21
  299. package/scripts/patch-app2.mjs +0 -18
  300. package/scripts/patch-dist-brief.mjs +0 -96
  301. package/scripts/patch-tool-exec.mjs +0 -70
  302. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  303. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  304. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  305. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  306. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  307. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  308. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  309. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  310. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  311. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  312. package/src/standalone/agent-task-status.test.mjs +0 -76
  313. package/src/tui/components/tool-output-format.test.mjs +0 -399
  314. package/src/tui/display-width.test.mjs +0 -35
  315. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  316. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  317. package/src/tui/input-editing.selection.test.mjs +0 -75
  318. package/src/tui/markdown/format-token.test.mjs +0 -354
  319. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  320. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  321. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  322. package/src/tui/paste-fix.test.mjs +0 -119
  323. package/src/tui/prompt-history-store.test.mjs +0 -52
  324. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  325. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  326. package/src/ui/markdown.test.mjs +0 -70
  327. package/src/ui/statusline-context-label.test.mjs +0 -15
  328. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  329. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -1,31 +1,23 @@
1
1
  import { classifyResultKind } from './result-classification.mjs';
2
- import { executeMcpTool, isMcpTool, mcpToolHasField } from '../mcp/client.mjs';
3
- import { canonicalizeBuiltinToolName, executeBuiltinTool, formatUnknownBuiltinToolMessage, isBuiltinTool, isExternalAdapterTool } from '../tools/builtin.mjs';
4
- import { executeBashSessionTool } from '../tools/bash-session.mjs';
5
- import { executePatchTool, takeApplyPatchUiDiff } from '../tools/patch.mjs';
2
+ import { canonicalizeBuiltinToolName, executeBuiltinTool, isBuiltinTool } from '../tools/builtin.mjs';
3
+ import { takeApplyPatchUiDiff } from '../tools/patch.mjs';
6
4
  import { executeInternalTool, isInternalTool } from '../internal-tools.mjs';
7
- import { normalizeToolEnvelope, makeToolEnvelope } from './tool-envelope.mjs';
5
+ import { normalizeToolEnvelope } from './tool-envelope.mjs';
8
6
  import { traceAgentLoop, traceAgentTool, traceAgentToolFailure, traceAgentCompact, estimateProviderPayloadBytes, messagePrefixHash, appendAgentTrace } from '../agent-trace.mjs';
9
- import { resolveSessionMaxLoopIterations } from '../agent-runtime/agent-loop-policy.mjs';
7
+ import { resolveSessionMaxLoopIterations, WORKER_SOFT_CAP_ITERATIONS, isWorkerSoftCapSession } from '../agent-runtime/agent-loop-policy.mjs';
10
8
  import { isAgentOwner } from '../agent-owner.mjs';
11
- import { markSessionToolCall, updateSessionStage, SessionClosedError, getSessionAbortSignal, enqueuePendingMessage, bumpUsageMetricsEpoch } from './manager.mjs';
9
+ import { markSessionToolCall, updateSessionStage, SessionClosedError, bumpUsageMetricsEpoch } from './manager.mjs';
12
10
  import {
13
- recallFastTrackCompactMessages,
14
11
  pruneToolOutputs,
15
12
  pruneToolOutputsUnanchored,
16
13
  semanticCompactMessages,
17
14
  effectiveBudget as compactEffectiveBudget,
18
15
  DEFAULT_COMPACT_TYPE,
19
- drainSessionCycle1,
20
- countRawPendingRows,
21
16
  } from './compact.mjs';
22
17
  import { isContextOverflowError } from '../providers/retry-classifier.mjs';
23
18
  import { stripSoftWarns } from '../tool-loop-guard.mjs';
24
19
  import { maybeOffloadToolResult } from './tool-result-offload.mjs';
25
20
  import { tryReadCached, setReadCached, invalidatePathForSession, markPostEdit, consumePostEditMark, clearReadDedupSession, extractTouchedPathsFromPatch, tryScopedToolCached, setScopedToolCached, clearScopedToolsForSession, clearScopedToolsForSessionPaths, invalidatePrefetchCache } from './read-dedup.mjs';
26
- import { createScopedCacheOutcome } from './cache/scoped-cache-outcome.mjs';
27
- import { modelVisibleToolCompletionMessage } from '../../../shared/tool-execution-contract.mjs';
28
- import { createHash } from 'crypto';
29
21
  import { isInvalidToolArgsMarker, formatInvalidToolArgsResult } from '../providers/openai-compat-stream.mjs';
30
22
 
31
23
  import {
@@ -37,13 +29,8 @@ import {
37
29
  _intraTurnSig,
38
30
  } from './loop/tool-classify.mjs';
39
31
  import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
40
- let codeGraphRuntimePromise = null;
41
- async function executeCodeGraphToolLazy(name, args, cwd, signal = null, options = {}) {
42
- codeGraphRuntimePromise ??= import('../tools/code-graph.mjs');
43
- const mod = await codeGraphRuntimePromise;
44
- if (typeof mod.executeCodeGraphTool !== 'function') throw new Error('code_graph runtime is not available');
45
- return mod.executeCodeGraphTool(name, args, cwd, signal, options);
46
- }
32
+ import { runRecallFastTrackCompact } from './loop/recall-fasttrack.mjs';
33
+ import { executeTool, _scopedCacheOutcomeForCall } from './loop/tool-exec.mjs';
47
34
 
48
35
  // classifyResultKind is imported from result-classification.mjs at the top of
49
36
  // this file; import it from there directly rather than via this module.
@@ -58,6 +45,13 @@ import {
58
45
  compactDebugLog,
59
46
  } from './loop/compact-debug.mjs';
60
47
  import { mergeSteeringEntries, steeringContentText } from './loop/steering.mjs';
48
+ import {
49
+ crossTurnSignature,
50
+ crossTurnDedupStub,
51
+ SOFT_CAP_WRAPUP_MESSAGE,
52
+ SOFT_CAP_REFUSAL_STUB,
53
+ } from './loop/completion-guards.mjs';
54
+ import { isEditProgressTool } from './loop/completion-guards.mjs';
61
55
  import { agentContextOverflowError } from './loop/context-overflow.mjs';
62
56
  import { positiveTokenInt } from './loop/env.mjs';
63
57
  import { normalizeUsage, addUsage } from './loop/usage.mjs';
@@ -76,12 +70,9 @@ import {
76
70
  isEagerDispatchable,
77
71
  messagesArrayChanged,
78
72
  getToolKind,
79
- buildSkillsListResponse,
80
- viewSkill,
81
73
  normalizeHookUpdatedToolOutput,
82
74
  resolveToolResultAfterHook,
83
75
  parseNativeToolSearchPayload,
84
- extractBashSessionId,
85
76
  buildAgentBashSessionArgs,
86
77
  formatMissingToolApprovalUiDenial,
87
78
  resolvePreToolAskApproval,
@@ -93,6 +84,8 @@ import {
93
84
  restoreToolCallBodyForId,
94
85
  } from './loop/stored-tool-args.mjs';
95
86
  import { repairTranscriptBeforeProviderSend } from './loop/transcript-repair.mjs';
87
+ import { classifyTerminationReason, INCOMPLETE_STOP_REASONS } from './loop/termination.mjs';
88
+ import { createSteeringLadder } from './loop/steering-ladder.mjs';
96
89
 
97
90
  // Facade re-exports: these symbols moved to split modules under ./loop/ but
98
91
  // remain part of loop.mjs's public surface (imported by scripts/tests and other
@@ -120,342 +113,8 @@ export {
120
113
  // this catches tight deterministic-failure loops (e.g. a command that errors
121
114
  // the same way every time) far earlier than 100 iterations.
122
115
  const REPEAT_FAIL_LIMIT = 3;
123
- async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTokens, compactPolicy, sessionId, signal }) {
124
- if (!sessionId) throw new Error('recall-fasttrack requires a session id');
125
- const startedAt = Date.now();
126
- const diagnostics = {
127
- hydrateLimit: null,
128
- ingestMs: null,
129
- ingestSkipped: false,
130
- ingestError: null,
131
- initialDumpMs: null,
132
- initialDumpBytes: null,
133
- initialDumpChars: null,
134
- initialRawPending: null,
135
- cycle1Ms: null,
136
- cycle1Skipped: false,
137
- cycle1SkipReason: null,
138
- cycle1Passes: null,
139
- cycle1RawRemaining: null,
140
- cycle1TextBytes: null,
141
- cycle1Error: null,
142
- finalRecallBytes: null,
143
- finalRecallChars: null,
144
- totalMs: null,
145
- };
146
- const query = `session:${sessionId}:all-chunks`;
147
- const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
148
- const callerCtx = {
149
- callerSessionId: sessionId || null,
150
- callerCwd: sessionRef?.cwd || undefined,
151
- routingSessionId: sessionId || null,
152
- clientHostPid: sessionRef?.clientHostPid,
153
- signal: signal || null,
154
- };
155
- const hydrateLimit = positiveTokenInt(sessionRef?.compaction?.recallIngestLimit)
156
- || Math.max(500, Math.min(5000, messages.length || 0));
157
- diagnostics.hydrateLimit = hydrateLimit;
158
- let t0 = Date.now();
159
- try {
160
- await executeInternalTool('memory', {
161
- action: 'ingest_session',
162
- sessionId,
163
- messages,
164
- cwd: sessionRef?.cwd,
165
- limit: hydrateLimit,
166
- }, callerCtx);
167
- } catch (err) {
168
- diagnostics.ingestSkipped = true;
169
- diagnostics.ingestError = compactDiagnosticError(err);
170
- try { process.stderr.write(`[loop] recall-fasttrack ingest skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
171
- } finally {
172
- diagnostics.ingestMs = Date.now() - t0;
173
- }
174
- const dumpArgs = {
175
- action: 'dump_session_roots',
176
- sessionId,
177
- includeRaw: true,
178
- limit: positiveTokenInt(sessionRef?.compaction?.recallChunkLimit ?? sessionRef?.compaction?.recallLimit) || hydrateLimit,
179
- };
180
- const runTool = (name, args) => executeInternalTool(name, args, callerCtx);
181
- t0 = Date.now();
182
- let recallText = await executeInternalTool('memory', dumpArgs, callerCtx);
183
- diagnostics.initialDumpMs = Date.now() - t0;
184
- diagnostics.initialDumpChars = String(recallText || '').length;
185
- diagnostics.initialDumpBytes = compactByteLength(recallText);
186
- diagnostics.initialRawPending = countRawPendingRows(recallText);
187
- let cycle1Text = '';
188
- const hasRawRows = /(?:^|\n)# raw_pending\s+\d+\s+id=/i.test(String(recallText || ''));
189
- if (hasRawRows) {
190
- t0 = Date.now();
191
- try {
192
- // Drain this session's cycle1 in window×concurrency units until no
193
- // raw rows remain, so the injected root is fully chunked rather than
194
- // carrying the unprocessed transcript tail (single-pass left raw in).
195
- const drained = await drainSessionCycle1(runTool, {
196
- sessionId,
197
- dumpArgs,
198
- deadlineMs: positiveTokenInt(sessionRef?.compaction?.recallCycle1DeadlineMs) || 120_000,
199
- maxPasses: positiveTokenInt(sessionRef?.compaction?.recallCycle1MaxPasses) || 0,
200
- cycleArgs: {
201
- min_batch: 1,
202
- session_cap: 1,
203
- batch_size: positiveTokenInt(sessionRef?.compaction?.recallCycle1BatchSize) || 100,
204
- rows_per_session: positiveTokenInt(sessionRef?.compaction?.recallRowsPerSession) || 100,
205
- window_size: positiveTokenInt(sessionRef?.compaction?.recallWindowSize) || 20,
206
- concurrency: positiveTokenInt(sessionRef?.compaction?.recallConcurrency) || 5,
207
- },
208
- });
209
- recallText = drained.recallText;
210
- cycle1Text = drained.cycle1Text;
211
- diagnostics.cycle1Passes = drained.passes;
212
- diagnostics.cycle1RawRemaining = drained.rawRemaining;
213
- diagnostics.cycle1TextBytes = compactByteLength(cycle1Text);
214
- if (drained.rawRemaining > 0) {
215
- try { process.stderr.write(`[loop] recall-fasttrack drained passes=${drained.passes} rawRemaining=${drained.rawRemaining} (sess=${sessionId || 'unknown'})\n`); } catch {}
216
- }
217
- } catch (err) {
218
- diagnostics.cycle1Error = compactDiagnosticError(err);
219
- try { process.stderr.write(`[loop] recall-fasttrack cycle1 skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
220
- } finally {
221
- diagnostics.cycle1Ms = Date.now() - t0;
222
- }
223
- } else {
224
- diagnostics.cycle1Skipped = true;
225
- diagnostics.cycle1SkipReason = 'session chunks already hydrated';
226
- diagnostics.cycle1Passes = 0;
227
- diagnostics.cycle1RawRemaining = 0;
228
- cycle1Text = 'cycle1: skipped (session chunks already hydrated)';
229
- }
230
- const combinedRecallText = [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n');
231
- diagnostics.finalRecallChars = combinedRecallText.length;
232
- diagnostics.finalRecallBytes = compactByteLength(combinedRecallText);
233
- const result = recallFastTrackCompactMessages(messages, compactBudgetTokens, {
234
- reserveTokens: compactPolicy.reserveTokens,
235
- force: true,
236
- recallText: combinedRecallText,
237
- query,
238
- querySha,
239
- allowEmptyRecall: true,
240
- tailTurns: compactPolicy.tailTurns,
241
- keepTokens: compactPolicy.keepTokens,
242
- preserveRecentTokens: compactPolicy.preserveRecentTokens,
243
- });
244
- diagnostics.totalMs = Date.now() - startedAt;
245
- if (result && typeof result === 'object') {
246
- result.diagnostics = {
247
- ...(result.diagnostics || {}),
248
- pipeline: diagnostics,
249
- };
250
- }
251
- compactDebugLog('recall-fasttrack pipeline', diagnostics);
252
- return result;
253
- }
254
- function _scopedCacheOutcomeForCall(sessionRef, toolCallId, toolName, callerSessionId, executeOpts = {}) {
255
- if (executeOpts.scopedCacheOutcome) {
256
- if (sessionRef && toolCallId) {
257
- if (!sessionRef._scopedCacheOutcomeByCallId) sessionRef._scopedCacheOutcomeByCallId = new Map();
258
- sessionRef._scopedCacheOutcomeByCallId.set(toolCallId, executeOpts.scopedCacheOutcome);
259
- }
260
- return executeOpts.scopedCacheOutcome;
261
- }
262
- if (!callerSessionId || !toolCallId || !_isScopedCacheableTool(toolName)) return null;
263
- const outcome = createScopedCacheOutcome();
264
- if (sessionRef) {
265
- if (!sessionRef._scopedCacheOutcomeByCallId) sessionRef._scopedCacheOutcomeByCallId = new Map();
266
- sessionRef._scopedCacheOutcomeByCallId.set(toolCallId, outcome);
267
- }
268
- return outcome;
269
- }
270
-
271
- async function executeTool(name, args, cwd, callerSessionId, sessionRef, executeOpts = {}) {
272
- const scopedCacheOutcome = _scopedCacheOutcomeForCall(
273
- sessionRef,
274
- executeOpts.toolCallId,
275
- name,
276
- callerSessionId,
277
- executeOpts,
278
- );
279
- const toolOpts = scopedCacheOutcome
280
- ? { ...executeOpts, scopedCacheOutcome }
281
- : executeOpts;
282
- const notificationSessionId = String(executeOpts.notifySessionId || sessionRef?.ownerSessionId || callerSessionId || '').trim();
283
- const notifyFn = typeof executeOpts.notifyFn === 'function'
284
- ? executeOpts.notifyFn
285
- : (text, meta = {}) => {
286
- if (!notificationSessionId) return;
287
- try {
288
- const visible = modelVisibleToolCompletionMessage(text, meta);
289
- if (visible) enqueuePendingMessage(notificationSessionId, visible);
290
- } catch { /* best effort */ }
291
- };
292
- const completionToolOpts = {
293
- ...toolOpts,
294
- sessionId: callerSessionId,
295
- callerSessionId: notificationSessionId || callerSessionId,
296
- routingSessionId: callerSessionId,
297
- clientHostPid: sessionRef?.clientHostPid,
298
- notifyFn,
299
- };
300
- const beforeToolHook = typeof executeOpts.beforeToolHook === 'function'
301
- ? executeOpts.beforeToolHook
302
- : sessionRef?.beforeToolHook;
303
- const toolApprovalHook = typeof executeOpts.toolApprovalHook === 'function'
304
- ? executeOpts.toolApprovalHook
305
- : sessionRef?.toolApprovalHook;
306
- if (beforeToolHook) {
307
- try {
308
- const decision = await beforeToolHook({
309
- name,
310
- args,
311
- cwd,
312
- sessionId: callerSessionId,
313
- toolCallId: executeOpts.toolCallId || null,
314
- });
315
- const action = String(decision?.action || decision?.decision || '').toLowerCase();
316
- if (action === 'deny' || action === 'block') {
317
- const reason = decision?.reason ? `: ${decision.reason}` : '';
318
- return `Error: tool "${name}" denied by hook${reason}`;
319
- }
320
- if (action === 'ask') {
321
- const askReason = String(decision?.reason || 'approval requested by hook').trim();
322
- const askOutcome = await resolvePreToolAskApproval({
323
- toolName: name,
324
- args,
325
- cwd,
326
- sessionId: callerSessionId,
327
- toolCallId: executeOpts.toolCallId || null,
328
- askReason,
329
- toolApprovalHook,
330
- });
331
- if (askOutcome.denial) return askOutcome.denial;
332
- const approval = askOutcome.approval;
333
- if (approval && typeof approval === 'object' && approval.args && typeof approval.args === 'object' && !Array.isArray(approval.args)) {
334
- args = approval.args;
335
- }
336
- }
337
- if ((action === 'modify' || action === 'rewrite') && decision?.args && typeof decision.args === 'object' && !Array.isArray(decision.args)) {
338
- args = decision.args;
339
- }
340
- } catch {
341
- // Hooks are policy extensions. A broken hook must not wedge the agent loop.
342
- }
343
- }
344
- const afterToolHook = typeof executeOpts.afterToolHook === 'function'
345
- ? executeOpts.afterToolHook
346
- : sessionRef?.afterToolHook;
347
- const __result = await (async () => {
348
- if (name === 'Skill') {
349
- return viewSkill(cwd, args?.name);
350
- }
351
- if (name === 'skills_list') {
352
- return buildSkillsListResponse(cwd);
353
- }
354
- if (name === 'skill_view') {
355
- return viewSkill(cwd, args?.name);
356
- }
357
- if (isMcpTool(name)) {
358
- // 24h trace data shows ~24% of external MCP calls are cwd-sensitive
359
- // (bash / grep / read / list / glob etc.) but the worker session's
360
- // cwd was previously dropped here. Inject cwd only when the tool's
361
- // inputSchema declares the field — schemas without it would reject
362
- // an unknown argument.
363
- const needsCwdInjection = cwd
364
- && mcpToolHasField(name, 'cwd')
365
- && (args == null || args.cwd == null);
366
- const finalArgs = needsCwdInjection ? { ...(args || {}), cwd } : args;
367
- return executeMcpTool(name, finalArgs);
368
- }
369
- if (name === 'code_graph') {
370
- // cwd chain: args.cwd (caller-explicit) → session cwd → undefined (handler throws)
371
- const graphCwd = (typeof args?.cwd === 'string' && args.cwd.trim()) ? args.cwd.trim() : cwd;
372
- return executeCodeGraphToolLazy(name, args, graphCwd, null, toolOpts);
373
- }
374
- if (isInternalTool(name)) {
375
- // callerSessionId propagates into server.mjs dispatchTool so that
376
- // dispatchAiWrapped can detect and reject recursive calls from a
377
- // hidden-role session (recall/search/explore → self).
378
- return executeInternalTool(name, args, {
379
- callerSessionId,
380
- callerCwd: cwd,
381
- clientHostPid: sessionRef?.clientHostPid,
382
- signal: executeOpts.signal,
383
- routingSessionId: callerSessionId,
384
- notifyFn,
385
- });
386
- }
387
- if (name === 'shell') {
388
- const routedArgs = buildAgentBashSessionArgs(args, sessionRef);
389
- if (!routedArgs) {
390
- // clientHostPid scopes background shell-jobs to the dispatching
391
- // terminal's claude.exe pid (agent sessions store it on sessionRef);
392
- // without it resolveJobOwnerHostPid falls back to the daemon-global env.
393
- return executeBuiltinTool(name, args, cwd, completionToolOpts);
394
- }
395
- // Thread the session's AbortSignal so agent type=close can interrupt the
396
- // persistent child process. getSessionAbortSignal is imported at top of
397
- // loop.mjs from manager.mjs; callerSessionId identifies the controller.
398
- let _bashAbortSignal = null;
399
- try { _bashAbortSignal = getSessionAbortSignal(callerSessionId); } catch { /* ignore */ }
400
- const result = await executeBashSessionTool('bash_session', routedArgs, cwd, {
401
- sessionId: callerSessionId,
402
- abortSignal: _bashAbortSignal,
403
- });
404
- const bashSid = extractBashSessionId(result);
405
- if (bashSid) {
406
- sessionRef.implicitBashSessionId = bashSid;
407
- // Track all persistent bash sessions for bulk teardown on close.
408
- if (sessionRef.allBashSessionIds) {
409
- if (!sessionRef.allBashSessionIds.includes(bashSid)) {
410
- sessionRef.allBashSessionIds.push(bashSid);
411
- }
412
- } else {
413
- sessionRef.allBashSessionIds = [bashSid];
414
- }
415
- }
416
- return result;
417
- }
418
- if (name === 'apply_patch') {
419
- const patchArgs = typeof args === 'string' ? { patch: args } : args;
420
- return executePatchTool(name, patchArgs, cwd, { sessionId: callerSessionId, toolCallId: executeOpts.toolCallId || null });
421
- }
422
- if (isBuiltinTool(name)) {
423
- // clientHostPid threaded for the same per-terminal job-scope reason as
424
- // the bash branch above (see resolveJobOwnerHostPid).
425
- return executeBuiltinTool(name, args, cwd, completionToolOpts);
426
- }
427
- if (isExternalAdapterTool(name)) {
428
- // Foreign-CLI tool names (StrReplace/Write/bash variants) adapt to a
429
- // native execution inside executeBuiltinTool's default: case; on a
430
- // shape mismatch it falls back to the redirect guidance message.
431
- return executeBuiltinTool(name, args, cwd, completionToolOpts);
432
- }
433
- return formatUnknownBuiltinToolMessage(name, args, 'tool');
434
- })();
435
- if (typeof afterToolHook === 'function') {
436
- try {
437
- const hookResult = await afterToolHook({
438
- name,
439
- args,
440
- cwd,
441
- sessionId: callerSessionId,
442
- toolCallId: executeOpts.toolCallId || null,
443
- result: __result,
444
- });
445
- // Envelope-aware hook override: a PostToolUse hook may override the
446
- // model-VISIBLE tool output (the envelope's `result` / stub), but it
447
- // must NEVER drop the `newMessages` channel. Split first, apply the
448
- // override to `result` only, then re-wrap so newMessages survive.
449
- const { result: __res, newMessages: __nm } = normalizeToolEnvelope(__result);
450
- const __overridden = resolveToolResultAfterHook(__res, hookResult);
451
- if (__nm.length) return makeToolEnvelope(__overridden, __nm);
452
- return __overridden;
453
- } catch {
454
- // PostToolUse hooks are best-effort; never let one break the tool result.
455
- }
456
- }
457
- return __result;
458
- }
116
+ // _scopedCacheOutcomeForCall and executeTool moved to ./loop/tool-exec.mjs
117
+ // (imported above).
459
118
  /**
460
119
  * Agent loop: send → tool_call → execute → re-send → repeat until text.
461
120
  * sendOpts may include:
@@ -472,10 +131,6 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
472
131
  // was not done — re-prompt instead of accepting empty as final.
473
132
  // Covers Anthropic (pause_turn, max_tokens), OpenAI (length), Gemini
474
133
  // (MAX_TOKENS, OTHER), and case variants.
475
- const INCOMPLETE_STOP_REASONS = new Set([
476
- 'pause_turn', 'max_tokens', 'length', 'MAX_TOKENS', 'OTHER',
477
- ]);
478
-
479
134
  export async function agentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts) {
480
135
  let iterations = 0;
481
136
  let toolCallsTotal = 0;
@@ -571,11 +226,57 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
571
226
  return true;
572
227
  };
573
228
  const maxLoopIterations = resolveSessionMaxLoopIterations(sessionRef);
229
+ // ---- Completion-first loop guards (worker runaway prevention) ----
230
+ // Step 1 (escalation ladder) + the missed-parallelism / serial-rewording
231
+ // steering hints live in the createSteeringLadder controller below; it owns
232
+ // their cumulative counters and emits at most one hint per turn.
233
+ // _editCount counts any executed tool call whose def lacks readOnlyHint
234
+ // (i.e. edit/progress: apply_patch, bash, MCP writes, skills, ...).
235
+ let _editCount = 0;
236
+ // Step 2: cross-turn identical read-only call dedup. Map keyed by
237
+ // signature(name + stableStringify(args)) → { count, firstIteration }.
238
+ // Populated only for SUCCESSFUL isEagerDispatchable (read-only) calls.
239
+ // Bounded to 500 entries (drop-oldest / insertion order).
240
+ const _crossTurnCalls = new Map();
241
+ const _CROSS_TURN_CAP = 500;
242
+ let _dedupStubTotal = 0;
243
+ // Step 3: worker soft-cap wrap-up state.
244
+ const _softCapEnabled = isWorkerSoftCapSession(sessionRef);
245
+ let _softCapActive = false; // tools disabled + wrap-up injected
246
+ let _softCapGraceTurns = 0; // text-only grace turns consumed (max 2)
247
+ let _terminatedBySoftCap = false;
248
+ // Completion-first steering ladder controller. Owns the (cumulative) level-1
249
+ // fire count, the all-read-only / serial-single / same-file-grep streaks,
250
+ // and the level-2 latch. Threaded via live getters so it reads the loop's
251
+ // current `iterations` / `_editCount` on every call (no stale snapshots).
252
+ const _steeringLadder = createSteeringLadder({
253
+ sessionId,
254
+ sessionAgent,
255
+ tools,
256
+ getIterations: () => iterations,
257
+ softCapEnabled: _softCapEnabled,
258
+ getEditCount: () => _editCount,
259
+ readOnlyRole: String(sessionRef?.permission || sessionRef?.toolPermission || '') === 'read',
260
+ pushUserMessage: (msg) => messages.push(msg),
261
+ pushSystemReminder: (text) => messages.push({ role: 'user', content: `<system-reminder>\n${text}\n</system-reminder>`, meta: 'hook' }),
262
+ });
574
263
  // Tool execution must use the session cwd even when the caller omitted the
575
264
  // legacy positional cwd argument. Agent workers always carry their cwd on
576
265
  // sessionRef; falling through to pwd()/process.cwd() resolves relatives
577
266
  // against the host/plugin root instead of the worker workspace.
578
267
  cwd = cwd || sessionRef?.cwd || undefined;
268
+ // Staged pre-cap warnings + one true hard stop. The ONLY count-based
269
+ // forced termination is the hard cap at maxLoopIterations (default 200):
270
+ // a genuine runaway guard. Before it, staged warnings fire at 50%/75%/90%
271
+ // of the cap steering the model to converge — warnings only, nothing is
272
+ // cut off early. All other runaway protection is behavior-based (steering
273
+ // ladder early wrap-up, REPEAT_FAIL_LIMIT), never a lower count.
274
+ let _iterWarnStage = 0;
275
+ const _iterWarnAt = [
276
+ Math.floor(maxLoopIterations * 0.5),
277
+ Math.floor(maxLoopIterations * 0.75),
278
+ Math.floor(maxLoopIterations * 0.9),
279
+ ];
579
280
  while (true) {
580
281
  throwIfAborted();
581
282
  if (iterations >= maxLoopIterations) {
@@ -583,6 +284,43 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
583
284
  terminatedByCap = true;
584
285
  break;
585
286
  }
287
+ if (_iterWarnStage < _iterWarnAt.length && iterations >= _iterWarnAt[_iterWarnStage]) {
288
+ _iterWarnStage += 1;
289
+ const warnAt = _iterWarnAt[_iterWarnStage - 1];
290
+ const stageMsg = _iterWarnStage === 1
291
+ ? `Iteration budget notice: ${warnAt} of ${maxLoopIterations} iterations used. Converge on a conclusion: prefer finishing the current objective over opening new exploration.`
292
+ : `Iteration budget warning (stage ${_iterWarnStage}): ${warnAt} of ${maxLoopIterations} iterations used — the loop hard-stops at ${maxLoopIterations}. Wrap up now: summarize progress, state what remains, and finish with your best current result.`;
293
+ messages.push({ role: 'user', content: `<system-reminder>\n${stageMsg}\n</system-reminder>`, meta: 'hook' });
294
+ process.stderr.write(`[loop] iteration warning stage ${_iterWarnStage} at ${iterations} (sess=${sessionId || 'unknown'}); continuing with steer.\n`);
295
+ try {
296
+ appendAgentTrace({
297
+ sessionId,
298
+ iteration: iterations,
299
+ kind: 'steer',
300
+ payload: { tag: 'iteration_warning', stage: _iterWarnStage, at: iterations, unit: maxLoopIterations },
301
+ agent: sessionAgent || null,
302
+ });
303
+ } catch { /* best-effort */ }
304
+ }
305
+ // Worker soft cap (Step 3): non-lead sessions that reach the soft-cap
306
+ // iteration count switch to a text-only wrap-up. On the FIRST crossing
307
+ // we disable tool defs (below, via _softCapActive) and inject the
308
+ // assistant-visible wrap-up directive as a user message so the next
309
+ // send produces a final text summary. Lead/TUI sessions never enter.
310
+ const _earlySoftCap = _steeringLadder.earlySoftCapArmed();
311
+ if (_softCapEnabled && !_softCapActive && (iterations >= WORKER_SOFT_CAP_ITERATIONS || _earlySoftCap)) {
312
+ _softCapActive = true;
313
+ messages.push({ role: 'user', content: `<system-reminder>\n${SOFT_CAP_WRAPUP_MESSAGE}\n</system-reminder>`, meta: 'hook' });
314
+ try {
315
+ appendAgentTrace({
316
+ sessionId,
317
+ iteration: iterations,
318
+ kind: 'steer',
319
+ payload: { tag: 'soft_cap_wrapup', soft_cap: WORKER_SOFT_CAP_ITERATIONS, early: _earlySoftCap, level2_fires: _steeringLadder.level2FireCount },
320
+ agent: sessionAgent || null,
321
+ });
322
+ } catch { /* best-effort */ }
323
+ }
586
324
  // Drain queued steering/prompts BEFORE the
587
325
  // pre-send compact check. The compact decision must see the exact
588
326
  // message set that the next provider.send would receive, including
@@ -614,6 +352,13 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
614
352
  const reactivePending = reactiveOverflowRetryPending === true;
615
353
  const shouldCompact = shouldCompactForSession(messageTokensEst, compactPolicy, { forceReactive: reactivePending });
616
354
  const pressureTokens = compactionTelemetryPressureTokens(messageTokensEst, compactPolicy, { reactivePending });
355
+ // A pending reactive-overflow retry makes THIS compact pass the
356
+ // recovery from a provider overflow refusal, not the proactive
357
+ // pressure trigger. Tag the emitted events so telemetry can tell
358
+ // them apart. Hoisted above the shouldCompact branch because the
359
+ // PostCompact hook below fires on BOTH paths (fixes a
360
+ // ReferenceError on the no-compact path).
361
+ const compactTrigger = reactivePending ? 'reactive' : 'auto';
617
362
  const compactBudgetTokens = shouldCompact
618
363
  ? (compactTargetBudget({ ...compactPolicy, pressureTokens }) || compactPolicy.boundaryTokens)
619
364
  : compactPolicy.boundaryTokens;
@@ -627,11 +372,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
627
372
  } else {
628
373
  try { opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
629
374
  const compactStartedAt = Date.now();
630
- // A pending reactive-overflow retry makes THIS compact pass the
631
- // recovery from a provider overflow refusal, not the proactive
632
- // pressure trigger. Tag the emitted events so telemetry can tell
633
- // them apart, then clear the one-shot flag.
634
- const compactTrigger = reactiveOverflowRetryPending ? 'reactive' : 'auto';
375
+ // Clear the one-shot reactive-overflow flag now that this
376
+ // compact pass is consuming it (compactTrigger already
377
+ // captured it above).
635
378
  reactiveOverflowRetryPending = false;
636
379
  // PreCompact: bridge to the standard hook bus before compaction
637
380
  // runs. session-property hook (manager/loop have no bus access).
@@ -992,7 +735,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
992
735
  } else {
993
736
  delete opts.toolChoice;
994
737
  }
995
- const sendTools = forcedFirstToolDef && toolCallsTotal === 0 ? [forcedFirstToolDef] : tools;
738
+ // Soft-cap wrap-up (Step 3a): once active, send NO tool definitions so
739
+ // the provider can only emit text. Overrides the forced-first-tool path.
740
+ const sendTools = _softCapActive
741
+ ? []
742
+ : (forcedFirstToolDef && toolCallsTotal === 0 ? [forcedFirstToolDef] : tools);
996
743
  // Eager-dispatch queue: when the provider streams a tool-call event,
997
744
  // start read-only tools immediately so execution overlaps with the
998
745
  // remaining SSE parse. Writes and unknown tools wait until send()
@@ -1033,6 +780,17 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1033
780
  const _rfg = sessionRef?._repeatFailGuard;
1034
781
  if (_rfg && _rfg.sig === _sig && _rfg.count >= REPEAT_FAIL_LIMIT) return null;
1035
782
  }
783
+ // Cross-turn dedup also gates eager dispatch (mirror of the
784
+ // repeat-failure guard above): a read-only call whose (name,args)
785
+ // signature already ran in an EARLIER turn must NOT be eagerly
786
+ // re-executed — the serial for-body pushes the [cross-turn-dedup]
787
+ // stub instead. Without this gate startEagerRun/onToolCall would
788
+ // re-run the call before the serial dedup check ever sees it.
789
+ {
790
+ const _ctSig = crossTurnSignature(call.name, call.arguments);
791
+ const _prior = _crossTurnCalls.get(_ctSig);
792
+ if (_prior && _prior.firstIteration < iterations) return null;
793
+ }
1036
794
  const toolKind = getToolKind(call.name);
1037
795
  // Shared pre-dispatch deny: identical predicate runs in the
1038
796
  // serial path below. If any role/permission guard would reject
@@ -1374,6 +1132,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1374
1132
  // tool-call-blocked vs contract-required oscillation.
1375
1133
  if (!response.toolCalls?.length) {
1376
1134
  // No tool calls. Decide between final-answer accept vs nudge.
1135
+ // Reviewer fix: a zero-tool turn (final-pre-send steering drain or
1136
+ // contract nudge `continue`) must not bridge the all-read-only
1137
+ // streak across non-tool turns — that would fire level-2 early on
1138
+ // a worker that paused to synthesize text mid-run.
1139
+ _steeringLadder.resetAllReadOnlyStreak();
1377
1140
  // - has content + non-hidden role → valid final, break.
1378
1141
  // - empty content + hidden role → contract allows text-only
1379
1142
  // terminal turn, break.
@@ -1447,6 +1210,37 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1447
1210
  : {}),
1448
1211
  };
1449
1212
  messages.push(_assistantTurnMsg);
1213
+ // Soft-cap wrap-up (Step 3b): tools are disabled but the model still
1214
+ // emitted tool calls. Do NOT execute them — push a refusal stub for
1215
+ // each (after the assistant turn is appended so tool_use/tool_result
1216
+ // pairing stays valid) and consume a grace turn. After 2 grace turns,
1217
+ // terminate via the soft-cap path so a model that never complies stops.
1218
+ if (_softCapActive) {
1219
+ for (const _c of calls) {
1220
+ pushToolResultMessage({
1221
+ role: 'tool',
1222
+ content: SOFT_CAP_REFUSAL_STUB,
1223
+ toolCallId: _c.id,
1224
+ toolKind: 'error',
1225
+ });
1226
+ }
1227
+ _softCapGraceTurns += 1;
1228
+ try {
1229
+ appendAgentTrace({
1230
+ sessionId,
1231
+ iteration: iterations,
1232
+ kind: 'steer',
1233
+ payload: { tag: 'soft_cap_wrapup', grace_turn: _softCapGraceTurns },
1234
+ agent: sessionAgent || null,
1235
+ });
1236
+ } catch { /* best-effort */ }
1237
+ if (_softCapGraceTurns >= 2) {
1238
+ _terminatedBySoftCap = true;
1239
+ break;
1240
+ }
1241
+ if (sessionId) updateSessionStage(sessionId, 'connecting');
1242
+ continue;
1243
+ }
1450
1244
  // Execute each tool and append results.
1451
1245
  //
1452
1246
  // Intra-turn duplicate suppression: when an LLM emits two tool_use
@@ -1501,6 +1295,42 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1501
1295
  });
1502
1296
  continue;
1503
1297
  }
1298
+ // Cross-turn identical-call stub (Step 2): a SUCCESSFUL read-only
1299
+ // (isEagerDispatchable) call whose (name,args) signature already ran
1300
+ // in an EARLIER turn is not re-executed — its result is unchanged and
1301
+ // already in context. Warn at the 2nd occurrence; append the "stuck"
1302
+ // escalation tail once the session has emitted 5+ dedup stubs total.
1303
+ // Never applies to write/bash/MCP/skill tools (not eager-dispatchable).
1304
+ if (isEagerDispatchable(call.name, tools)) {
1305
+ const _ctSig = crossTurnSignature(call.name, call.arguments);
1306
+ const _prior = _crossTurnCalls.get(_ctSig);
1307
+ if (_prior && _prior.firstIteration < iterations) {
1308
+ _prior.count += 1;
1309
+ _dedupStubTotal += 1;
1310
+ const _stub = crossTurnDedupStub(call.name, _prior.firstIteration, _dedupStubTotal >= 5);
1311
+ pushToolResultMessage({
1312
+ role: 'tool',
1313
+ content: _stub,
1314
+ toolCallId: call.id,
1315
+ });
1316
+ try {
1317
+ appendAgentTrace({
1318
+ sessionId,
1319
+ iteration: iterations,
1320
+ kind: 'steer',
1321
+ payload: {
1322
+ tag: 'cross_turn_dedup',
1323
+ tool: call.name,
1324
+ occurrence: _prior.count,
1325
+ first_iteration: _prior.firstIteration,
1326
+ dedup_stub_total: _dedupStubTotal,
1327
+ },
1328
+ agent: sessionAgent || null,
1329
+ });
1330
+ } catch { /* best-effort */ }
1331
+ continue;
1332
+ }
1333
+ }
1504
1334
  // Cross-iteration repeat-failure guard. Distinct from the
1505
1335
  // intra-turn dedup above (which spans ONE assistant turn and
1506
1336
  // resets every turn): when the model re-issues an IDENTICAL
@@ -1660,7 +1490,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1660
1490
  // success path below (_executeOk && _resultKind==='normal'). A failed or
1661
1491
  // errored call would otherwise leak its entry in
1662
1492
  // sessionRef._scopedCacheOutcomeByCallId forever — reclaim it here.
1663
- if (sessionRef?._scopedCacheOutcomeByCallId && call?.id && (!_executeOk || _resultKind === 'error')) {
1493
+ if (sessionRef?._scopedCacheOutcomeByCallId instanceof Map && call?.id && (!_executeOk || _resultKind === 'error')) {
1664
1494
  sessionRef._scopedCacheOutcomeByCallId.delete(call.id);
1665
1495
  }
1666
1496
  // PostToolUseFailure: a tool that resolved to a failure (thrown-error
@@ -1851,7 +1681,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1851
1681
  // body via the disk path in that stub.
1852
1682
  if (sessionId && _executeOk && _resultKind === 'normal') {
1853
1683
  if (_scopedCacheHit === null && _isScopedCacheableTool(call.name)) {
1854
- const _outcome = sessionRef?._scopedCacheOutcomeByCallId?.get(call.id);
1684
+ const _outcomeMap = sessionRef?._scopedCacheOutcomeByCallId instanceof Map
1685
+ ? sessionRef._scopedCacheOutcomeByCallId : null;
1686
+ const _outcome = _outcomeMap?.get(call.id);
1855
1687
  setScopedToolCached({
1856
1688
  sessionId,
1857
1689
  toolName: _toolBare,
@@ -1861,7 +1693,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1861
1693
  toolUseId: call.id,
1862
1694
  complete: _outcome ? _outcome.complete : true,
1863
1695
  });
1864
- sessionRef?._scopedCacheOutcomeByCallId?.delete(call.id);
1696
+ _outcomeMap?.delete(call.id);
1865
1697
  }
1866
1698
  if (_readCacheHit === null && _isReadTool(call.name)) {
1867
1699
  // Pass tool_use id so future cache-hits can reference the body's location in history.
@@ -1885,8 +1717,43 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1885
1717
  ...(_nativeToolSearch ? { nativeToolSearch: _nativeToolSearch } : {}),
1886
1718
  ...(_applyPatchUiDiff ? { uiDiff: _applyPatchUiDiff } : {}),
1887
1719
  });
1720
+ // Completion-first bookkeeping (Steps 1 & 2). Only successful
1721
+ // executions count. Edit/progress = any executed tool whose def
1722
+ // lacks readOnlyHint (apply_patch/bash/MCP-write/skill/...).
1723
+ // Read-only successful calls seed the cross-turn dedup map.
1724
+ if (_executeOk) {
1725
+ const _isEager = isEagerDispatchable(call.name, tools);
1726
+ if (_isEager) {
1727
+ const _ctSig = crossTurnSignature(call.name, call.arguments);
1728
+ if (!_crossTurnCalls.has(_ctSig)) {
1729
+ _crossTurnCalls.set(_ctSig, { count: 1, firstIteration: iterations });
1730
+ if (_crossTurnCalls.size > _CROSS_TURN_CAP) {
1731
+ const _oldest = _crossTurnCalls.keys().next().value;
1732
+ _crossTurnCalls.delete(_oldest);
1733
+ }
1734
+ }
1735
+ } else {
1736
+ // A successful mutating (non-eager) tool invalidates the
1737
+ // cross-turn dedup map wholesale: any prior read/grep may
1738
+ // now return different content, so a post-edit
1739
+ // verification read must NOT be stubbed as "unchanged".
1740
+ if (isEditProgressTool(call.name, false)) {
1741
+ _crossTurnCalls.clear();
1742
+ _editCount += 1;
1743
+ }
1744
+ }
1745
+ }
1888
1746
  } catch (postErr) {
1889
1747
  _postProcessOk = false;
1748
+ // Reviewer fix: the exec itself succeeded — if it was a
1749
+ // mutating edit-progress tool, the file changes are real even
1750
+ // though post-processing failed, so the cross-turn dedup map
1751
+ // must still be invalidated (otherwise a later verification
1752
+ // read could be stubbed as "unchanged" against stale sigs).
1753
+ if (_executeOk && !isEagerDispatchable(call.name, tools) && isEditProgressTool(call.name, false)) {
1754
+ _crossTurnCalls.clear();
1755
+ _editCount += 1;
1756
+ }
1890
1757
  // Post-processing failed AFTER a successful exec: the result is
1891
1758
  // replaced with an error below, so preserve this call's full body
1892
1759
  // too for a clean retry (mirrors the failed-exec path above).
@@ -1961,6 +1828,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1961
1828
  } catch { /* best-effort: PostToolBatch hook must never break the loop */ }
1962
1829
  }
1963
1830
  }
1831
+ // Completion-first steering hints (missed-parallelism / all-read-only /
1832
+ // serial-rewording). At most ONE hint per turn (priority: soft-cap >
1833
+ // level-2 > same-file grep > level-1); soft-cap active suppresses all.
1834
+ // The ladder controller owns the cumulative counters and streaks.
1835
+ _steeringLadder.emitPostBatchSteering(calls, _softCapActive);
1964
1836
  // Mid-turn steering is drained at the next loop's pre-send point,
1965
1837
  // AFTER any auto-compact pass. Draining here would put the steering
1966
1838
  // user turn after the fresh tool results before compaction runs; then
@@ -1971,31 +1843,13 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1971
1843
  }
1972
1844
  // Classify WHY the loop ended so agent-tool can promote an empty/abnormal
1973
1845
  // finish to an explicit Lead-facing error instead of a silent empty
1974
- // "completed". Determine "has content" exactly the way the no-tool-call
1975
- // branch above does (trimmed string content, or any reasoning content).
1976
- const _finalHasContent = (typeof response?.content === 'string' && response.content.trim().length > 0)
1977
- || (typeof response?.reasoningContent === 'string' && response.reasoningContent.trim().length > 0);
1978
- const _finalStopReason = response?.stopReason ?? response?.stop_reason ?? null;
1979
- const _finalIncompleteStop = _finalStopReason && INCOMPLETE_STOP_REASONS.has(_finalStopReason);
1980
- const _finalIsHidden = HIDDEN_AGENT_NAMES.has(sessionAgent);
1981
- let terminationReason;
1982
- if (terminatedByCap) {
1983
- // Real problem regardless of hidden/public: the loop never terminated
1984
- // on its own contract.
1985
- terminationReason = 'iteration_cap';
1986
- } else if (!_finalHasContent && _finalIncompleteStop) {
1987
- // Cut short mid-synthesis (token cap / provider pause). Real problem
1988
- // for hidden agents too.
1989
- terminationReason = 'truncated';
1990
- } else if (!_finalHasContent && !_finalIsHidden) {
1991
- // Empty terminal turn. Only public agents violate their contract by
1992
- // finishing empty — hidden agents (explorer/cycle/…) legitimately emit
1993
- // text-only/empty terminal turns per their own role contract, so leave
1994
- // terminationReason undefined for them.
1995
- terminationReason = 'empty';
1996
- } else {
1997
- terminationReason = undefined;
1998
- }
1846
+ // "completed" (see classifyTerminationReason in ./loop/termination.mjs).
1847
+ const terminationReason = classifyTerminationReason(response, {
1848
+ terminatedByCap,
1849
+ terminatedBySoftCap: _terminatedBySoftCap,
1850
+ softCapActive: _softCapActive,
1851
+ sessionAgent,
1852
+ });
1999
1853
  return {
2000
1854
  ...response,
2001
1855
  usage: lastUsage || response.usage,