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
@@ -13,11 +13,17 @@ import {
13
13
  normalizeCompactType,
14
14
  DEFAULT_COMPACT_TYPE,
15
15
  DEFAULT_COMPACTION_KEEP_TOKENS,
16
+ CONTEXT_SHARE_RATIO,
16
17
  } from '../compact.mjs';
17
18
  import { positiveTokenInt, envFlag, envTokenInt } from './env.mjs';
19
+ import { isAgentOwner } from '../../agent-owner.mjs';
18
20
 
19
21
  const COMPACT_SAFETY_PERCENT = 1.00;
20
- const COMPACT_TARGET_RATIO = 0.02;
22
+ // Unified context-share rule (compact/constants.mjs CONTEXT_SHARE_RATIO): the
23
+ // post-compaction target is 5% of the boundary/context window — the same 5%
24
+ // the recall-fasttrack injection cap uses (loop.mjs recallTokenCap). One
25
+ // number governs every "share of model context" budget.
26
+ const COMPACT_TARGET_RATIO = CONTEXT_SHARE_RATIO;
21
27
  const COMPACT_TARGET_MIN_TOKENS = 4_000;
22
28
  const COMPACT_TARGET_MAX_TOKENS = 16_000;
23
29
 
@@ -30,7 +36,13 @@ function resolveSemanticCompactSetting(sessionRef, cfg = {}) {
30
36
  return true;
31
37
  }
32
38
 
33
- function resolveCompactTypeSetting(_sessionRef, cfg = {}) {
39
+ function resolveCompactTypeSetting(sessionRef, cfg = {}) {
40
+ // Agent-owned sessions are ALWAYS semantic. recall-fasttrack rebuilds
41
+ // context from Memory recall, which is scoped to the user's main-session
42
+ // history — an agent's tool-loop history is not in the recall pool, so a
43
+ // fasttrack compact would inject unrelated main-session memories and drop
44
+ // the agent's own working context. Env/config overrides do not apply.
45
+ if (isAgentOwner(sessionRef)) return DEFAULT_COMPACT_TYPE;
34
46
  const configured = process.env.MIXDOG_AGENT_COMPACT_TYPE
35
47
  ?? process.env.MIXDOG_COMPACT_TYPE
36
48
  ?? cfg.type
@@ -0,0 +1,61 @@
1
+ // Completion-first loop guards: escalation ladder (level-2 steering),
2
+ // cross-turn identical read-only call dedup, and worker soft-cap wrap-up.
3
+ // Pure string/signature helpers extracted from loop.mjs so the loop body only
4
+ // wires state + messages. No provider/manager coupling.
5
+
6
+ // Deterministic, key-sorted stringify for cross-turn call signatures. Mirrors
7
+ // _canonicalArgs but exposed by name for the dedup signature contract.
8
+ export function stableStringify(value) {
9
+ if (value == null || typeof value !== 'object') {
10
+ try { return JSON.stringify(value); } catch { return String(value); }
11
+ }
12
+ if (Array.isArray(value)) {
13
+ try { return `[${value.map(stableStringify).join(',')}]`; } catch { return String(value); }
14
+ }
15
+ try {
16
+ const keys = Object.keys(value).sort();
17
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
18
+ } catch { return String(value); }
19
+ }
20
+
21
+ export function crossTurnSignature(name, args) {
22
+ return `${name}:${stableStringify(args)}`;
23
+ }
24
+
25
+ // Tool names that are non-eager (no readOnlyHint) but are NOT edits/progress —
26
+ // they must not reset the escalation ladder's "zero edit" condition. Skill /
27
+ // recall / agent / task / cwd / tool_search are exploration/meta plumbing.
28
+ const NON_PROGRESS_TOOLS = new Set(['Skill', 'recall', 'agent', 'task', 'cwd', 'tool_search']);
29
+
30
+ // True when a successfully-executed tool represents real edit/progress. A tool
31
+ // counts as progress only if its def lacks readOnlyHint (not eager) AND it is
32
+ // not in the meta/non-progress set. apply_patch and shell/bash always count.
33
+ export function isEditProgressTool(name, isEager) {
34
+ if (isEager) return false;
35
+ const bare = name && name.startsWith('mcp__') ? name.split('__').pop() : name;
36
+ if (bare === 'apply_patch' || bare === 'shell' || bare === 'bash' || bare === 'bash_session') return true;
37
+ return !NON_PROGRESS_TOOLS.has(bare);
38
+ }
39
+
40
+ // Step 1 — level-2 escalation steering. N = cumulative level-1 fires.
41
+ // `readOnlyRole` swaps the edit-oriented directive for a report-oriented one:
42
+ // read-permission sessions (reviewer-style) cannot apply_patch, so telling
43
+ // them to edit is self-contradictory and pushes premature termination.
44
+ export function level2SteerMessage(n, readOnlyRole = false) {
45
+ if (readOnlyRole) {
46
+ return `<system-reminder>\nYou have received this batching reminder ${n} times. Converge now: report your findings from what you have already read, or state exactly what information is missing for a verdict. A partial report with named gaps is a valid, successful completion — continued exploration is not.\n</system-reminder>`;
47
+ }
48
+ return `<system-reminder>\nYou have received this batching reminder ${n} times without making any edit. Stop exploring now: either apply_patch with what you already know, or return a blocked report stating exactly what is missing. A blocked report is a valid, successful completion — continued exploration is not.\n</system-reminder>`;
49
+ }
50
+
51
+ // Step 2 — cross-turn dedup stub. `stuck` appends the escalation tail at the
52
+ // 5th+ dedup stub in the session.
53
+ export function crossTurnDedupStub(name, firstIteration, stuck) {
54
+ let s = `[cross-turn-dedup] identical read-only \`${name}\` call already executed in iteration ${firstIteration}; its result is unchanged and already in context. Use it, or change path/offset/pattern for new information.`;
55
+ if (stuck) s += ` Repeated identical calls indicate you are stuck — apply what you know or return blocked.`;
56
+ return s;
57
+ }
58
+
59
+ // Step 3 — worker soft-cap wrap-up assistant-visible directive + refusal stub.
60
+ export const SOFT_CAP_WRAPUP_MESSAGE = `MAXIMUM EXPLORATION BUDGET REACHED. Tools are now disabled. Respond with text only: summarize work completed so far, list remaining/incomplete items, and state what is blocking or what should be done next. This overrides all other instructions.`;
61
+ export const SOFT_CAP_REFUSAL_STUB = `Tools are disabled: exploration budget reached. Provide your final text summary.`;
@@ -20,9 +20,7 @@ const WORKER_DENIED_TOOLS = new Set([
20
20
  // session control.
21
21
  'agent',
22
22
  // channels module (owner/Discord-facing)
23
- 'reply', 'react', 'edit_message', 'download_attachment', 'fetch',
24
- 'schedule_status', 'trigger_schedule', 'schedule_control',
25
- 'activate_channel_bridge', 'reload_config', 'inject_command',
23
+ 'reply', 'fetch',
26
24
  // host input injection
27
25
  'inject_input',
28
26
  ]);
@@ -0,0 +1,182 @@
1
+ // Recall-fasttrack compaction pipeline, extracted from loop.mjs.
2
+ // Hydrates the session transcript into the memory pipeline (ingest_session),
3
+ // dumps chunked/raw roots, drains cycle1 until no raw rows remain, and folds
4
+ // the combined recall text back into a compacted message array capped at
5
+ // CONTEXT_SHARE_RATIO of the model context window. No behavior change: this is
6
+ // the same body that lived inline in loop.mjs, re-exported via the facade so
7
+ // existing importers keep working.
8
+ import { createHash } from 'crypto';
9
+ import { executeInternalTool } from '../../internal-tools.mjs';
10
+ import { loadConfig as loadOrchestratorConfig } from '../../config.mjs';
11
+ import {
12
+ recallFastTrackCompactMessages,
13
+ CONTEXT_SHARE_RATIO,
14
+ RECALL_TOKEN_CAP_FLOOR_TOKENS,
15
+ drainSessionCycle1,
16
+ countRawPendingRows,
17
+ } from '../compact.mjs';
18
+ import {
19
+ compactDiagnosticError,
20
+ compactByteLength,
21
+ compactDebugLog,
22
+ } from './compact-debug.mjs';
23
+ import { positiveTokenInt } from './env.mjs';
24
+
25
+ export async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTokens, compactPolicy, sessionId, signal }) {
26
+ if (!sessionId) throw new Error('recall-fasttrack requires a session id');
27
+ const startedAt = Date.now();
28
+ const diagnostics = {
29
+ hydrateLimit: null,
30
+ ingestMs: null,
31
+ ingestSkipped: false,
32
+ ingestError: null,
33
+ initialDumpMs: null,
34
+ initialDumpBytes: null,
35
+ initialDumpChars: null,
36
+ initialRawPending: null,
37
+ cycle1Ms: null,
38
+ cycle1Skipped: false,
39
+ cycle1SkipReason: null,
40
+ cycle1Passes: null,
41
+ cycle1RawRemaining: null,
42
+ cycle1TextBytes: null,
43
+ cycle1Error: null,
44
+ finalRecallBytes: null,
45
+ finalRecallChars: null,
46
+ totalMs: null,
47
+ };
48
+ const query = `session:${sessionId}:all-chunks`;
49
+ const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
50
+ const callerCtx = {
51
+ callerSessionId: sessionId || null,
52
+ callerCwd: sessionRef?.cwd || undefined,
53
+ routingSessionId: sessionId || null,
54
+ clientHostPid: sessionRef?.clientHostPid,
55
+ signal: signal || null,
56
+ };
57
+ const hydrateLimit = positiveTokenInt(sessionRef?.compaction?.recallIngestLimit)
58
+ || Math.max(500, Math.min(5000, messages.length || 0));
59
+ diagnostics.hydrateLimit = hydrateLimit;
60
+ let t0 = Date.now();
61
+ try {
62
+ await executeInternalTool('memory', {
63
+ action: 'ingest_session',
64
+ sessionId,
65
+ messages,
66
+ cwd: sessionRef?.cwd,
67
+ limit: hydrateLimit,
68
+ }, callerCtx);
69
+ } catch (err) {
70
+ diagnostics.ingestSkipped = true;
71
+ diagnostics.ingestError = compactDiagnosticError(err);
72
+ try { process.stderr.write(`[loop] recall-fasttrack ingest skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
73
+ } finally {
74
+ diagnostics.ingestMs = Date.now() - t0;
75
+ }
76
+ const dumpArgs = {
77
+ action: 'dump_session_roots',
78
+ sessionId,
79
+ includeRaw: true,
80
+ limit: positiveTokenInt(sessionRef?.compaction?.recallChunkLimit ?? sessionRef?.compaction?.recallLimit) || hydrateLimit,
81
+ };
82
+ const runTool = (name, args) => executeInternalTool(name, args, callerCtx);
83
+ t0 = Date.now();
84
+ let recallText = await executeInternalTool('memory', dumpArgs, callerCtx);
85
+ diagnostics.initialDumpMs = Date.now() - t0;
86
+ diagnostics.initialDumpChars = String(recallText || '').length;
87
+ diagnostics.initialDumpBytes = compactByteLength(recallText);
88
+ diagnostics.initialRawPending = countRawPendingRows(recallText);
89
+ let cycle1Text = '';
90
+ const hasRawRows = /(?:^|\n)# raw_pending\s+\d+\s+id=/i.test(String(recallText || ''));
91
+ // Recap off = NO memory-pipeline LLM calls: skip the cycle1 drain entirely
92
+ // and let the dump's raw transcript lines (includeRaw:true above) ride into
93
+ // the injected summary as-is. Poll-on-use: re-read the flag per compact so
94
+ // a runtime toggle applies without restart. Default on if config read fails.
95
+ let recapOn = true;
96
+ try { recapOn = loadOrchestratorConfig({ secrets: false })?.recap?.enabled !== false; } catch { /* default on */ }
97
+ if (hasRawRows && !recapOn) {
98
+ diagnostics.cycle1Skipped = true;
99
+ diagnostics.cycle1SkipReason = 'recap disabled';
100
+ diagnostics.cycle1Passes = 0;
101
+ diagnostics.cycle1RawRemaining = countRawPendingRows(recallText);
102
+ cycle1Text = 'cycle1: skipped (recap disabled — raw transcript lines kept as-is)';
103
+ } else if (hasRawRows) {
104
+ t0 = Date.now();
105
+ try {
106
+ // Drain this session's cycle1 in window×concurrency units until no
107
+ // raw rows remain, so the injected root is fully chunked rather than
108
+ // carrying the unprocessed transcript tail (single-pass left raw in).
109
+ const drained = await drainSessionCycle1(runTool, {
110
+ sessionId,
111
+ dumpArgs,
112
+ deadlineMs: positiveTokenInt(sessionRef?.compaction?.recallCycle1DeadlineMs) || 120_000,
113
+ maxPasses: positiveTokenInt(sessionRef?.compaction?.recallCycle1MaxPasses) || 0,
114
+ cycleArgs: {
115
+ min_batch: 1,
116
+ session_cap: 1,
117
+ batch_size: positiveTokenInt(sessionRef?.compaction?.recallCycle1BatchSize) || 100,
118
+ rows_per_session: positiveTokenInt(sessionRef?.compaction?.recallRowsPerSession) || 100,
119
+ window_size: positiveTokenInt(sessionRef?.compaction?.recallWindowSize) || 20,
120
+ concurrency: positiveTokenInt(sessionRef?.compaction?.recallConcurrency) || 5,
121
+ },
122
+ });
123
+ recallText = drained.recallText;
124
+ cycle1Text = drained.cycle1Text;
125
+ diagnostics.cycle1Passes = drained.passes;
126
+ diagnostics.cycle1RawRemaining = drained.rawRemaining;
127
+ diagnostics.cycle1TextBytes = compactByteLength(cycle1Text);
128
+ if (drained.error) {
129
+ diagnostics.cycle1Error = drained.error;
130
+ try { process.stderr.write(`[loop] recall-fasttrack cycle1 error (sess=${sessionId || 'unknown'}): ${drained.error}\n`); } catch {}
131
+ }
132
+ if (drained.rawRemaining > 0) {
133
+ try { process.stderr.write(`[loop] recall-fasttrack drained passes=${drained.passes} rawRemaining=${drained.rawRemaining} (sess=${sessionId || 'unknown'})\n`); } catch {}
134
+ }
135
+ } catch (err) {
136
+ diagnostics.cycle1Error = compactDiagnosticError(err);
137
+ try { process.stderr.write(`[loop] recall-fasttrack cycle1 skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
138
+ } finally {
139
+ diagnostics.cycle1Ms = Date.now() - t0;
140
+ }
141
+ } else {
142
+ diagnostics.cycle1Skipped = true;
143
+ diagnostics.cycle1SkipReason = 'session chunks already hydrated';
144
+ diagnostics.cycle1Passes = 0;
145
+ diagnostics.cycle1RawRemaining = 0;
146
+ cycle1Text = 'cycle1: skipped (session chunks already hydrated)';
147
+ }
148
+ const combinedRecallText = [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n');
149
+ diagnostics.finalRecallChars = combinedRecallText.length;
150
+ diagnostics.finalRecallBytes = compactByteLength(combinedRecallText);
151
+ // Recall injection (chunked-summary + raw-fallback text, combined above)
152
+ // never exceeds CONTEXT_SHARE_RATIO (5%) of the model context window
153
+ // (floor RECALL_TOKEN_CAP_FLOOR_TOKENS); the rest of the budget belongs to
154
+ // live conversation. Same unified 5% as the compact target ratio
155
+ // (compact-policy.mjs COMPACT_TARGET_RATIO). Omitted when contextWindow is
156
+ // unknown/0 so current no-cap behavior is preserved.
157
+ const _recallCapWindow = Number(compactPolicy.contextWindow) || 0;
158
+ const recallTokenCap = _recallCapWindow > 0
159
+ ? Math.max(RECALL_TOKEN_CAP_FLOOR_TOKENS, Math.floor(_recallCapWindow * CONTEXT_SHARE_RATIO))
160
+ : undefined;
161
+ const result = recallFastTrackCompactMessages(messages, compactBudgetTokens, {
162
+ reserveTokens: compactPolicy.reserveTokens,
163
+ force: true,
164
+ recallText: combinedRecallText,
165
+ query,
166
+ querySha,
167
+ allowEmptyRecall: true,
168
+ tailTurns: compactPolicy.tailTurns,
169
+ keepTokens: compactPolicy.keepTokens,
170
+ preserveRecentTokens: compactPolicy.preserveRecentTokens,
171
+ recallTokenCap,
172
+ });
173
+ diagnostics.totalMs = Date.now() - startedAt;
174
+ if (result && typeof result === 'object') {
175
+ result.diagnostics = {
176
+ ...(result.diagnostics || {}),
177
+ pipeline: diagnostics,
178
+ };
179
+ }
180
+ compactDebugLog('recall-fasttrack pipeline', diagnostics);
181
+ return result;
182
+ }
@@ -0,0 +1,173 @@
1
+ // Completion-first steering ladder (worker runaway prevention), extracted from
2
+ // loop.mjs. Owns the mutable ladder counters and the post-batch steering-hint
3
+ // emitters. State is threaded live via a context object of getters/setters so
4
+ // no snapshot goes stale — the loop mutates `messages`/`iterations` in place and
5
+ // this module reads them through the accessors on each call. No behavior change:
6
+ // the counters, thresholds, and emitted messages are verbatim from agentLoop.
7
+ import { appendAgentTrace } from '../../agent-trace.mjs';
8
+ import { level2SteerMessage } from './completion-guards.mjs';
9
+ import { isEagerDispatchable } from './tool-helpers.mjs';
10
+
11
+ // Consecutive ignored level-2 steers (zero edits) that force the wrap-up early.
12
+ export const EARLY_SOFT_CAP_LEVEL2_FIRES = 3;
13
+
14
+ // Build the completion-first steering-ladder controller. `ctx` supplies live
15
+ // accessors so every read reflects the loop's current mutable state:
16
+ // - messages, sessionId, sessionAgent, tools (stable refs/values)
17
+ // - getIterations() (current iteration)
18
+ // - softCapEnabled (constant per loop)
19
+ // - getEditCount() (mutated by the loop)
20
+ // - pushSystemReminder(text) → push a meta:'hook' user message
21
+ // - pushUserMessage(msg) → push a raw user message (level-2 latch text)
22
+ export function createSteeringLadder(ctx) {
23
+ const {
24
+ sessionId,
25
+ sessionAgent,
26
+ tools,
27
+ getIterations,
28
+ softCapEnabled,
29
+ getEditCount,
30
+ } = ctx;
31
+ const pushSystemReminder = ctx.pushSystemReminder;
32
+ const pushUserMessage = ctx.pushUserMessage;
33
+ // Permission-based role detection (agent names are user-definable):
34
+ // read-permission sessions legitimately never edit, so they get the
35
+ // report-oriented level-2 text and never arm the early soft-cap.
36
+ const readOnlyRole = ctx.readOnlyRole === true;
37
+
38
+ // Step 1: escalation ladder. _level1FireCount is CUMULATIVE (never reset)
39
+ // so repeated batching reminders accumulate across the whole session.
40
+ // _level2LatchAtIteration latches level-2 steering to at most once / 5 turns.
41
+ let _level1FireCount = 0;
42
+ let _level2LatchAtIteration = -Infinity;
43
+ // Independent ladder counter: consecutive turns where EVERY call is
44
+ // read-only (any count) with zero edits. Catches multi-call read-only
45
+ // turns that the single-call level-1 streak misses. Reset on any edit.
46
+ let _allReadOnlyStreak = 0;
47
+ // Tracks consecutive assistant turns that ran exactly one read-only tool
48
+ // call (missed parallelism). Not reset per-iteration — only by the
49
+ // steering-hint fire below or by a turn that batches/edits.
50
+ let _serialReadOnlyStreak = 0;
51
+ // Tracks consecutive grep calls scoped to the SAME path (any patterns) —
52
+ // the "serial rewording" spiral: re-grepping one file with reworded
53
+ // patterns instead of reading it. Only counts turns whose every call is a
54
+ // grep on that path; any other tool/path resets it.
55
+ let _sameFileGrepStreak = 0;
56
+ let _sameFileGrepPath = null;
57
+ // Early soft-cap: N ignored level-2 steers with zero edits forces the
58
+ // wrap-up early; any edit disarms it (level-2 requires editCount === 0).
59
+ let _level2FireCount = 0;
60
+
61
+ // Level-2 steering emitter shared by both ladder paths (single-call
62
+ // level-1 streak and the independent all-read-only streak). Sets the latch
63
+ // so it fires at most once per 5 turns regardless of which path triggered.
64
+ const _emitLevel2Steer = () => {
65
+ const iterations = getIterations();
66
+ _level2LatchAtIteration = iterations;
67
+ _level2FireCount += 1;
68
+ // When this fire arms the early soft-cap, the wrap-up injected at the
69
+ // next loop head supersedes the level-2 text — skip the redundant hint.
70
+ const _armsEarlyCap = !readOnlyRole && softCapEnabled && _level2FireCount >= EARLY_SOFT_CAP_LEVEL2_FIRES && getEditCount() === 0;
71
+ if (!_armsEarlyCap) pushUserMessage({ role: 'user', content: level2SteerMessage(_level1FireCount, readOnlyRole), meta: 'hook' });
72
+ try {
73
+ appendAgentTrace({
74
+ sessionId,
75
+ iteration: iterations,
76
+ kind: 'steer',
77
+ payload: { tag: 'level2_steer', level1_fires: _level1FireCount, level2_fires: _level2FireCount, edit_count: getEditCount(), all_read_only_streak: _allReadOnlyStreak },
78
+ agent: sessionAgent || null,
79
+ });
80
+ } catch { /* best-effort */ }
81
+ };
82
+
83
+ return {
84
+ // Loop head reads: is the early soft-cap armed?
85
+ get level2FireCount() { return _level2FireCount; },
86
+ earlySoftCapArmed() {
87
+ return !readOnlyRole && _level2FireCount >= EARLY_SOFT_CAP_LEVEL2_FIRES && getEditCount() === 0;
88
+ },
89
+ // Post-batch steering hint gate. `hintAlreadyFired` seeds the once-per-
90
+ // turn latch (soft-cap active suppresses all hints). Returns nothing;
91
+ // pushes at most one steering message via the ctx push callbacks.
92
+ emitPostBatchSteering(calls, hintAlreadyFired) {
93
+ const iterations = getIterations();
94
+ const editCount = getEditCount();
95
+ // Steering hint gate: at most ONE hint per turn (priority: soft-cap >
96
+ // level-2 > same-file grep > level-1), and none once the soft-cap
97
+ // wrap-up is active — its "text only" directive must not share a send
98
+ // with a "keep exploring" hint.
99
+ let _hintFiredThisTurn = hintAlreadyFired;
100
+ // Missed-parallelism steering: 3+ consecutive turns of a single
101
+ // read-only tool call suggest the model isn't batching independent
102
+ // lookups. Nudge once, then reset (fires again after 3 more).
103
+ if (calls.length === 1 && isEagerDispatchable(calls[0].name, tools)) {
104
+ _serialReadOnlyStreak += 1;
105
+ if (_serialReadOnlyStreak >= 3 && !_hintFiredThisTurn) {
106
+ _serialReadOnlyStreak = 0;
107
+ // Escalation ladder (Step 1). Cumulative level-1 fires are
108
+ // tracked and NEVER reset. Once level-1 has fired >=3 times with
109
+ // ZERO edits, escalate to level-2 steering (blocked-report is a
110
+ // valid completion) instead of the batching nudge — latched to at
111
+ // most once per 5 turns.
112
+ _level1FireCount += 1;
113
+ if (_level1FireCount >= 3 && editCount === 0 && (iterations - _level2LatchAtIteration) >= 5) {
114
+ _emitLevel2Steer();
115
+ } else {
116
+ pushSystemReminder('Last 3 turns each ran a single read-only tool. Batch independent lookups (read/grep/glob/code_graph) into ONE turn, or start editing if you have enough context.');
117
+ }
118
+ _hintFiredThisTurn = true;
119
+ }
120
+ } else {
121
+ _serialReadOnlyStreak = 0;
122
+ }
123
+ // Independent all-read-only escalation (audit finding): the level-1
124
+ // streak above only counts single-call turns, so a worker that runs
125
+ // 2+ read-only calls per turn escapes the ladder entirely. Track a
126
+ // cumulative count of consecutive turns where EVERY call is read-only
127
+ // (any count) and no edit has been made; at 12 such turns fire level-2
128
+ // directly (same once-per-5-turn latch), reset on any edit.
129
+ {
130
+ const _allReadOnly = calls.length > 0 && calls.every((c) => isEagerDispatchable(c.name, tools));
131
+ if (_allReadOnly && editCount === 0) {
132
+ _allReadOnlyStreak += 1;
133
+ if (_allReadOnlyStreak >= 12 && (iterations - _level2LatchAtIteration) >= 5 && !_hintFiredThisTurn) {
134
+ _emitLevel2Steer();
135
+ _hintFiredThisTurn = true;
136
+ }
137
+ } else {
138
+ _allReadOnlyStreak = 0;
139
+ }
140
+ }
141
+ // Serial-rewording steering: 4+ consecutive turns grepping the SAME
142
+ // path with reworded patterns = a search spiral that single-call
143
+ // batching cannot catch. Crisis-only: fires once per spiral, then
144
+ // resets. Read-the-file is almost always the answer at that point.
145
+ {
146
+ const _grepPathOf = (c) => {
147
+ if (c?.name !== 'grep') return null;
148
+ const p = c?.arguments?.path;
149
+ return typeof p === 'string' && p ? p : null;
150
+ };
151
+ const _turnPaths = calls.map(_grepPathOf);
152
+ const _uniq = [...new Set(_turnPaths)];
153
+ if (_uniq.length === 1 && _uniq[0] !== null) {
154
+ if (_uniq[0] === _sameFileGrepPath) _sameFileGrepStreak += 1;
155
+ else { _sameFileGrepPath = _uniq[0]; _sameFileGrepStreak = 1; }
156
+ if (_sameFileGrepStreak >= 4 && !_hintFiredThisTurn) {
157
+ pushSystemReminder(`4+ consecutive grep turns on the same path (${_sameFileGrepPath}). Rewording patterns is not converging — read the relevant span directly (read with offset/limit) or act on what you have.`);
158
+ _sameFileGrepStreak = 0;
159
+ _sameFileGrepPath = null;
160
+ _hintFiredThisTurn = true;
161
+ }
162
+ } else {
163
+ _sameFileGrepStreak = 0;
164
+ _sameFileGrepPath = null;
165
+ }
166
+ }
167
+ },
168
+ // Reviewer fix: a zero-tool turn must not bridge the all-read-only
169
+ // streak across non-tool turns — that would fire level-2 early on a
170
+ // worker that paused to synthesize text mid-run.
171
+ resetAllReadOnlyStreak() { _allReadOnlyStreak = 0; },
172
+ };
173
+ }
@@ -0,0 +1,58 @@
1
+ // Loop termination-reason classification, extracted from loop.mjs.
2
+ // Pure function over the final response + loop-end flags. No behavior change:
3
+ // the classification ladder is verbatim from the tail of agentLoop.
4
+ import { HIDDEN_AGENT_NAMES } from './hidden-agents.mjs';
5
+
6
+ // Stop reasons that signal the turn was cut short mid-synthesis (token cap,
7
+ // provider pause). Empty content + one of these reasons means the worker
8
+ // was not done. Covers Anthropic (pause_turn, max_tokens), OpenAI (length),
9
+ // Gemini (MAX_TOKENS, OTHER), and case variants.
10
+ export const INCOMPLETE_STOP_REASONS = new Set([
11
+ 'pause_turn', 'max_tokens', 'length', 'MAX_TOKENS', 'OTHER',
12
+ ]);
13
+
14
+ // Classify WHY the loop ended so agent-tool can promote an empty/abnormal
15
+ // finish to an explicit Lead-facing error instead of a silent empty
16
+ // "completed". Determine "has content" exactly the way the no-tool-call
17
+ // branch in agentLoop does (trimmed string content, or any reasoning content).
18
+ export function classifyTerminationReason(response, {
19
+ terminatedByCap,
20
+ terminatedBySoftCap,
21
+ softCapActive,
22
+ sessionAgent,
23
+ } = {}) {
24
+ const _finalHasContent = (typeof response?.content === 'string' && response.content.trim().length > 0)
25
+ || (typeof response?.reasoningContent === 'string' && response.reasoningContent.trim().length > 0);
26
+ const _finalStopReason = response?.stopReason ?? response?.stop_reason ?? null;
27
+ const _finalIncompleteStop = _finalStopReason && INCOMPLETE_STOP_REASONS.has(_finalStopReason);
28
+ const _finalIsHidden = HIDDEN_AGENT_NAMES.has(sessionAgent);
29
+ if (terminatedByCap) {
30
+ // Real problem regardless of hidden/public: the loop never terminated
31
+ // on its own contract.
32
+ return 'iteration_cap';
33
+ }
34
+ if (terminatedBySoftCap || softCapActive) {
35
+ // Worker soft-cap wrap-up path: non-lead session hit the exploration
36
+ // budget and was steered to a text-only finish. Distinct from the hard
37
+ // cap (iteration_cap) — this is an expected, budget-driven termination,
38
+ // not a runaway. Covers BOTH the grace-exhausted path
39
+ // (terminatedBySoftCap) AND the compliant path where the model emitted
40
+ // the final text with no more tool calls while the soft cap was active
41
+ // (loop ends via the no-tool-call break, so terminatedBySoftCap stays
42
+ // false but softCapActive is still true).
43
+ return 'soft_cap_wrapup';
44
+ }
45
+ if (!_finalHasContent && _finalIncompleteStop) {
46
+ // Cut short mid-synthesis (token cap / provider pause). Real problem
47
+ // for hidden agents too.
48
+ return 'truncated';
49
+ }
50
+ if (!_finalHasContent && !_finalIsHidden) {
51
+ // Empty terminal turn. Only public agents violate their contract by
52
+ // finishing empty — hidden agents (explorer/cycle/…) legitimately emit
53
+ // text-only/empty terminal turns per their own role contract, so leave
54
+ // terminationReason undefined for them.
55
+ return 'empty';
56
+ }
57
+ return undefined;
58
+ }