mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. 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,275 @@
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
+ import { TOOL_OUTPUT_MAX_BYTES } from '../../tools/builtin/tool-output-limit.mjs';
25
+
26
+ // ── Digest mode (compaction.recallDigest=true) ─────────────────────────────
27
+ // Instead of folding the FULL chunked session dump into the compacted
28
+ // messages (heavy: cycle1 drain + up to CONTEXT_SHARE_RATIO of the context
29
+ // window), inject a small newest-first digest plus an instruction telling the
30
+ // model to pull details lazily via recall(sessionId/query/period). The memory
31
+ // DB already holds the full session (ingest_session below runs in both
32
+ // modes), and raw rows are embedded synchronously at ingest, so recall serves
33
+ // everything the big injection used to carry.
34
+ // Default digest cap = the SHARED tool-output limit (TOOL_OUTPUT_MAX_BYTES,
35
+ // 50KB default, env MIXDOG_TOOL_OUTPUT_MAX_BYTES) — the digest injection is
36
+ // budgeted like any other tool result, not a special context share.
37
+ // compaction.recallDigestMaxKb still overrides per-session.
38
+ const DIGEST_DEFAULT_MAX_KB = Math.max(1, Math.floor(TOOL_OUTPUT_MAX_BYTES / 1024));
39
+
40
+ // Byte-capped line-boundary truncation. Digest source is newest-first, so
41
+ // keeping the HEAD keeps the newest turns.
42
+ function truncateToKb(text, maxKb) {
43
+ const maxBytes = Math.max(1, maxKb) * 1024;
44
+ const s = String(text || '');
45
+ if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;
46
+ const lines = s.split('\n');
47
+ const out = [];
48
+ let used = 0;
49
+ for (const line of lines) {
50
+ const cost = Buffer.byteLength(line, 'utf8') + 1;
51
+ if (used + cost > maxBytes) break;
52
+ out.push(line);
53
+ used += cost;
54
+ }
55
+ return out.join('\n') + '\n[digest truncated at ' + maxKb + 'KB — pull the rest via recall]';
56
+ }
57
+
58
+ function buildRecallDigestText(sessionId, digestBody, maxKb) {
59
+ // No recall-usage instruction block here: the recall tool description
60
+ // already carries the usage-pattern cheatsheet (tool-defs.mjs), so
61
+ // repeating it per-compaction would be redundant injected tokens. The
62
+ // one-line header marks the compaction boundary and names the session id
63
+ // the model needs for a scoped recall.
64
+ return [
65
+ `[context compacted — session ${sessionId}]`,
66
+ `Full history is in memory — use the recall tool for details beyond this digest.`,
67
+ `Recent digest (newest first):`,
68
+ truncateToKb(digestBody, maxKb),
69
+ ].join('\n');
70
+ }
71
+
72
+ export async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTokens, compactPolicy, sessionId, signal }) {
73
+ if (!sessionId) throw new Error('recall-fasttrack requires a session id');
74
+ const startedAt = Date.now();
75
+ const diagnostics = {
76
+ hydrateLimit: null,
77
+ ingestMs: null,
78
+ ingestSkipped: false,
79
+ ingestError: null,
80
+ initialDumpMs: null,
81
+ initialDumpBytes: null,
82
+ initialDumpChars: null,
83
+ initialRawPending: null,
84
+ cycle1Ms: null,
85
+ cycle1Skipped: false,
86
+ cycle1SkipReason: null,
87
+ cycle1Passes: null,
88
+ cycle1RawRemaining: null,
89
+ cycle1TextBytes: null,
90
+ cycle1Error: null,
91
+ finalRecallBytes: null,
92
+ finalRecallChars: null,
93
+ totalMs: null,
94
+ };
95
+ const query = `session:${sessionId}:all-chunks`;
96
+ const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
97
+ const callerCtx = {
98
+ callerSessionId: sessionId || null,
99
+ callerCwd: sessionRef?.cwd || undefined,
100
+ routingSessionId: sessionId || null,
101
+ clientHostPid: sessionRef?.clientHostPid,
102
+ signal: signal || null,
103
+ };
104
+ const hydrateLimit = positiveTokenInt(sessionRef?.compaction?.recallIngestLimit)
105
+ || Math.max(500, Math.min(5000, messages.length || 0));
106
+ diagnostics.hydrateLimit = hydrateLimit;
107
+ let t0 = Date.now();
108
+ try {
109
+ await executeInternalTool('memory', {
110
+ action: 'ingest_session',
111
+ sessionId,
112
+ messages,
113
+ cwd: sessionRef?.cwd,
114
+ limit: hydrateLimit,
115
+ }, callerCtx);
116
+ } catch (err) {
117
+ diagnostics.ingestSkipped = true;
118
+ diagnostics.ingestError = compactDiagnosticError(err);
119
+ try { process.stderr.write(`[loop] recall-fasttrack ingest skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
120
+ } finally {
121
+ diagnostics.ingestMs = Date.now() - t0;
122
+ }
123
+ // ── Digest mode: skip the dump + cycle1 drain entirely. Pull a small
124
+ // newest-first session browse (recall path: roots + raw fallback merged
125
+ // chronologically), cap it at recallDigestMaxKb, and inject it with a
126
+ // recall-usage instruction. The model pulls older/topical detail lazily.
127
+ const digestMode = sessionRef?.compaction?.recallDigest === true;
128
+ if (digestMode) {
129
+ const digestMaxKb = positiveTokenInt(sessionRef?.compaction?.recallDigestMaxKb) || DIGEST_DEFAULT_MAX_KB;
130
+ let digestBody = '';
131
+ t0 = Date.now();
132
+ try {
133
+ const browsed = await executeInternalTool('memory', {
134
+ action: 'search',
135
+ sessionId,
136
+ limit: positiveTokenInt(sessionRef?.compaction?.recallDigestLimit) || 30,
137
+ includeMembers: true,
138
+ }, callerCtx);
139
+ digestBody = typeof browsed === 'string' ? browsed : String(browsed?.text ?? browsed ?? '');
140
+ } catch (err) {
141
+ diagnostics.cycle1Error = compactDiagnosticError(err);
142
+ try { process.stderr.write(`[loop] recall-digest browse failed (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
143
+ }
144
+ diagnostics.initialDumpMs = Date.now() - t0;
145
+ diagnostics.cycle1Skipped = true;
146
+ diagnostics.cycle1SkipReason = 'digest mode';
147
+ diagnostics.cycle1Passes = 0;
148
+ const digestText = buildRecallDigestText(sessionId, digestBody, digestMaxKb);
149
+ diagnostics.finalRecallChars = digestText.length;
150
+ diagnostics.finalRecallBytes = compactByteLength(digestText);
151
+ const result = recallFastTrackCompactMessages(messages, compactBudgetTokens, {
152
+ reserveTokens: compactPolicy.reserveTokens,
153
+ force: true,
154
+ recallText: digestText,
155
+ query,
156
+ querySha,
157
+ allowEmptyRecall: true,
158
+ tailTurns: compactPolicy.tailTurns,
159
+ keepTokens: compactPolicy.keepTokens,
160
+ preserveRecentTokens: compactPolicy.preserveRecentTokens,
161
+ });
162
+ diagnostics.totalMs = Date.now() - startedAt;
163
+ if (result && typeof result === 'object') {
164
+ result.diagnostics = { ...(result.diagnostics || {}), pipeline: { ...diagnostics, digestMode: true } };
165
+ }
166
+ compactDebugLog('recall-digest pipeline', diagnostics);
167
+ return result;
168
+ }
169
+ const dumpArgs = {
170
+ action: 'dump_session_roots',
171
+ sessionId,
172
+ includeRaw: true,
173
+ limit: positiveTokenInt(sessionRef?.compaction?.recallChunkLimit ?? sessionRef?.compaction?.recallLimit) || hydrateLimit,
174
+ };
175
+ const runTool = (name, args) => executeInternalTool(name, args, callerCtx);
176
+ t0 = Date.now();
177
+ let recallText = await executeInternalTool('memory', dumpArgs, callerCtx);
178
+ diagnostics.initialDumpMs = Date.now() - t0;
179
+ diagnostics.initialDumpChars = String(recallText || '').length;
180
+ diagnostics.initialDumpBytes = compactByteLength(recallText);
181
+ diagnostics.initialRawPending = countRawPendingRows(recallText);
182
+ let cycle1Text = '';
183
+ const hasRawRows = /(?:^|\n)# raw_pending\s+\d+\s+id=/i.test(String(recallText || ''));
184
+ // Recap off = NO memory-pipeline LLM calls: skip the cycle1 drain entirely
185
+ // and let the dump's raw transcript lines (includeRaw:true above) ride into
186
+ // the injected summary as-is. Poll-on-use: re-read the flag per compact so
187
+ // a runtime toggle applies without restart. Default on if config read fails.
188
+ let recapOn = true;
189
+ try { recapOn = loadOrchestratorConfig({ secrets: false })?.recap?.enabled !== false; } catch { /* default on */ }
190
+ if (hasRawRows && !recapOn) {
191
+ diagnostics.cycle1Skipped = true;
192
+ diagnostics.cycle1SkipReason = 'recap disabled';
193
+ diagnostics.cycle1Passes = 0;
194
+ diagnostics.cycle1RawRemaining = countRawPendingRows(recallText);
195
+ cycle1Text = 'cycle1: skipped (recap disabled — raw transcript lines kept as-is)';
196
+ } else if (hasRawRows) {
197
+ t0 = Date.now();
198
+ try {
199
+ // Drain this session's cycle1 in window×concurrency units until no
200
+ // raw rows remain, so the injected root is fully chunked rather than
201
+ // carrying the unprocessed transcript tail (single-pass left raw in).
202
+ const drained = await drainSessionCycle1(runTool, {
203
+ sessionId,
204
+ dumpArgs,
205
+ deadlineMs: positiveTokenInt(sessionRef?.compaction?.recallCycle1DeadlineMs) || 120_000,
206
+ maxPasses: positiveTokenInt(sessionRef?.compaction?.recallCycle1MaxPasses) || 0,
207
+ cycleArgs: {
208
+ min_batch: 1,
209
+ session_cap: 1,
210
+ batch_size: positiveTokenInt(sessionRef?.compaction?.recallCycle1BatchSize) || 100,
211
+ rows_per_session: positiveTokenInt(sessionRef?.compaction?.recallRowsPerSession) || 100,
212
+ window_size: positiveTokenInt(sessionRef?.compaction?.recallWindowSize) || 20,
213
+ concurrency: positiveTokenInt(sessionRef?.compaction?.recallConcurrency) || 5,
214
+ },
215
+ });
216
+ recallText = drained.recallText;
217
+ cycle1Text = drained.cycle1Text;
218
+ diagnostics.cycle1Passes = drained.passes;
219
+ diagnostics.cycle1RawRemaining = drained.rawRemaining;
220
+ diagnostics.cycle1TextBytes = compactByteLength(cycle1Text);
221
+ if (drained.error) {
222
+ diagnostics.cycle1Error = drained.error;
223
+ try { process.stderr.write(`[loop] recall-fasttrack cycle1 error (sess=${sessionId || 'unknown'}): ${drained.error}\n`); } catch {}
224
+ }
225
+ if (drained.rawRemaining > 0) {
226
+ try { process.stderr.write(`[loop] recall-fasttrack drained passes=${drained.passes} rawRemaining=${drained.rawRemaining} (sess=${sessionId || 'unknown'})\n`); } catch {}
227
+ }
228
+ } catch (err) {
229
+ diagnostics.cycle1Error = compactDiagnosticError(err);
230
+ try { process.stderr.write(`[loop] recall-fasttrack cycle1 skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
231
+ } finally {
232
+ diagnostics.cycle1Ms = Date.now() - t0;
233
+ }
234
+ } else {
235
+ diagnostics.cycle1Skipped = true;
236
+ diagnostics.cycle1SkipReason = 'session chunks already hydrated';
237
+ diagnostics.cycle1Passes = 0;
238
+ diagnostics.cycle1RawRemaining = 0;
239
+ cycle1Text = 'cycle1: skipped (session chunks already hydrated)';
240
+ }
241
+ const combinedRecallText = [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n');
242
+ diagnostics.finalRecallChars = combinedRecallText.length;
243
+ diagnostics.finalRecallBytes = compactByteLength(combinedRecallText);
244
+ // Recall injection (chunked-summary + raw-fallback text, combined above)
245
+ // never exceeds CONTEXT_SHARE_RATIO (5%) of the model context window
246
+ // (floor RECALL_TOKEN_CAP_FLOOR_TOKENS); the rest of the budget belongs to
247
+ // live conversation. Same unified 5% as the compact target ratio
248
+ // (compact-policy.mjs COMPACT_TARGET_RATIO). Omitted when contextWindow is
249
+ // unknown/0 so current no-cap behavior is preserved.
250
+ const _recallCapWindow = Number(compactPolicy.contextWindow) || 0;
251
+ const recallTokenCap = _recallCapWindow > 0
252
+ ? Math.max(RECALL_TOKEN_CAP_FLOOR_TOKENS, Math.floor(_recallCapWindow * CONTEXT_SHARE_RATIO))
253
+ : undefined;
254
+ const result = recallFastTrackCompactMessages(messages, compactBudgetTokens, {
255
+ reserveTokens: compactPolicy.reserveTokens,
256
+ force: true,
257
+ recallText: combinedRecallText,
258
+ query,
259
+ querySha,
260
+ allowEmptyRecall: true,
261
+ tailTurns: compactPolicy.tailTurns,
262
+ keepTokens: compactPolicy.keepTokens,
263
+ preserveRecentTokens: compactPolicy.preserveRecentTokens,
264
+ recallTokenCap,
265
+ });
266
+ diagnostics.totalMs = Date.now() - startedAt;
267
+ if (result && typeof result === 'object') {
268
+ result.diagnostics = {
269
+ ...(result.diagnostics || {}),
270
+ pipeline: diagnostics,
271
+ };
272
+ }
273
+ compactDebugLog('recall-fasttrack pipeline', diagnostics);
274
+ return result;
275
+ }
@@ -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
+ }