mixdog 0.9.1 → 0.9.3

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 (301) hide show
  1. package/package.json +9 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  6. package/scripts/background-task-meta-smoke.mjs +1 -1
  7. package/scripts/bench-run.mjs +262 -0
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/compact-smoke.mjs +12 -0
  10. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  11. package/scripts/explore-bench.mjs +124 -0
  12. package/scripts/hook-bus-test.mjs +191 -0
  13. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  14. package/scripts/internal-comms-bench.mjs +727 -0
  15. package/scripts/internal-comms-smoke.mjs +75 -0
  16. package/scripts/lead-workflow-smoke.mjs +4 -4
  17. package/scripts/live-worker-smoke.mjs +9 -9
  18. package/scripts/output-style-bench.mjs +285 -0
  19. package/scripts/output-style-smoke.mjs +13 -10
  20. package/scripts/patch-replay.mjs +90 -0
  21. package/scripts/path-suffix-test.mjs +57 -0
  22. package/scripts/provider-stream-stall-test.mjs +276 -0
  23. package/scripts/provider-toolcall-test.mjs +599 -1
  24. package/scripts/recall-bench.mjs +207 -0
  25. package/scripts/routing-corpus.mjs +281 -0
  26. package/scripts/session-bench.mjs +1526 -0
  27. package/scripts/session-diag.mjs +595 -0
  28. package/scripts/task-bench.mjs +207 -0
  29. package/scripts/tool-failures.mjs +6 -6
  30. package/scripts/tool-smoke.mjs +310 -67
  31. package/scripts/toolcall-args-test.mjs +81 -0
  32. package/src/agents/debugger/AGENT.md +4 -4
  33. package/src/agents/heavy-worker/AGENT.md +20 -9
  34. package/src/agents/reviewer/AGENT.md +4 -4
  35. package/src/agents/worker/AGENT.md +17 -9
  36. package/src/app.mjs +10 -6
  37. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  38. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  39. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  40. package/src/headless-role.mjs +14 -14
  41. package/src/help.mjs +1 -0
  42. package/src/lib/rules-builder.cjs +32 -54
  43. package/src/mixdog-session-runtime.mjs +1040 -2036
  44. package/src/output-styles/default.md +12 -7
  45. package/src/output-styles/minimal.md +25 -0
  46. package/src/output-styles/oneline.md +21 -0
  47. package/src/output-styles/simple.md +10 -9
  48. package/src/repl.mjs +17 -7
  49. package/src/rules/agent/00-common.md +7 -5
  50. package/src/rules/agent/30-explorer.md +8 -12
  51. package/src/rules/lead/01-general.md +3 -1
  52. package/src/rules/lead/lead-tool.md +13 -2
  53. package/src/rules/shared/01-tool.md +23 -12
  54. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  55. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  59. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  60. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
  62. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  63. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
  65. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  66. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
  70. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
  71. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
  72. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  73. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  74. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  75. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  76. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
  78. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
  79. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
  80. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  82. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
  83. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
  85. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  86. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  87. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  88. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  89. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  90. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  91. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  93. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  94. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  97. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  98. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  99. package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
  100. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  101. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  102. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  103. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  104. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  105. package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
  106. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  107. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  108. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  109. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  110. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  113. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  118. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  119. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  121. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
  123. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  124. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  125. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
  126. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  127. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  129. package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
  130. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  131. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  132. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  135. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  136. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  137. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  138. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  139. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  140. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  141. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  142. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  148. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  149. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  150. package/src/runtime/channels/backends/discord.mjs +99 -9
  151. package/src/runtime/channels/backends/telegram.mjs +501 -0
  152. package/src/runtime/channels/index.mjs +437 -1439
  153. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  154. package/src/runtime/channels/lib/config.mjs +54 -2
  155. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  156. package/src/runtime/channels/lib/format.mjs +4 -2
  157. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  158. package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
  159. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  160. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  161. package/src/runtime/channels/lib/telegram-format.mjs +280 -0
  162. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  164. package/src/runtime/channels/lib/webhook.mjs +59 -31
  165. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  166. package/src/runtime/channels/tool-defs.mjs +1 -1
  167. package/src/runtime/memory/index.mjs +465 -345
  168. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  169. package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
  170. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  171. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  172. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  173. package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
  174. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  175. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  176. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  177. package/src/runtime/memory/lib/memory.mjs +121 -4
  178. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  179. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  180. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  181. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  182. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  183. package/src/runtime/memory/tool-defs.mjs +10 -7
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/background-tasks.mjs +2 -3
  186. package/src/runtime/shared/buffered-appender.mjs +149 -0
  187. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  188. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  189. package/src/runtime/shared/config.mjs +9 -0
  190. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  191. package/src/runtime/shared/schedules-store.mjs +21 -19
  192. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  193. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  194. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  195. package/src/runtime/shared/tool-surface.mjs +98 -13
  196. package/src/runtime/shared/transcript-writer.mjs +156 -0
  197. package/src/runtime/shared/update-checker.mjs +214 -0
  198. package/src/session-runtime/config-helpers.mjs +209 -0
  199. package/src/session-runtime/effort.mjs +128 -0
  200. package/src/session-runtime/fs-utils.mjs +10 -0
  201. package/src/session-runtime/model-capabilities.mjs +130 -0
  202. package/src/session-runtime/output-styles.mjs +124 -0
  203. package/src/session-runtime/plugin-mcp.mjs +114 -0
  204. package/src/session-runtime/session-text.mjs +100 -0
  205. package/src/session-runtime/statusline-route.mjs +35 -0
  206. package/src/session-runtime/tool-catalog.mjs +720 -0
  207. package/src/session-runtime/workflow.mjs +358 -0
  208. package/src/standalone/agent-tool.mjs +302 -117
  209. package/src/standalone/channel-admin.mjs +133 -40
  210. package/src/standalone/channel-worker.mjs +10 -292
  211. package/src/standalone/explore-tool.mjs +12 -5
  212. package/src/standalone/hook-bus.mjs +165 -8
  213. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  214. package/src/standalone/opencode-go-login.mjs +121 -0
  215. package/src/standalone/provider-admin.mjs +25 -3
  216. package/src/standalone/usage-dashboard.mjs +1 -1
  217. package/src/tui/App.jsx +2883 -778
  218. package/src/tui/components/ConfirmBar.jsx +47 -0
  219. package/src/tui/components/ContextPanel.jsx +5 -3
  220. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  221. package/src/tui/components/Markdown.jsx +22 -98
  222. package/src/tui/components/Message.jsx +14 -35
  223. package/src/tui/components/Picker.jsx +87 -12
  224. package/src/tui/components/PromptInput.jsx +355 -22
  225. package/src/tui/components/QueuedCommands.jsx +1 -1
  226. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  227. package/src/tui/components/Spinner.jsx +7 -7
  228. package/src/tui/components/StatusLine.jsx +40 -21
  229. package/src/tui/components/TextEntryPanel.jsx +51 -7
  230. package/src/tui/components/ToolExecution.jsx +183 -101
  231. package/src/tui/components/TurnDone.jsx +4 -4
  232. package/src/tui/components/UsagePanel.jsx +1 -1
  233. package/src/tui/components/tool-output-format.mjs +161 -23
  234. package/src/tui/components/tool-output-format.test.mjs +87 -0
  235. package/src/tui/display-width.mjs +69 -0
  236. package/src/tui/display-width.test.mjs +35 -0
  237. package/src/tui/dist/index.mjs +6731 -2333
  238. package/src/tui/engine/agent-envelope.mjs +296 -0
  239. package/src/tui/engine/boot-profile.mjs +21 -0
  240. package/src/tui/engine/labels.mjs +67 -0
  241. package/src/tui/engine/notice-text.mjs +112 -0
  242. package/src/tui/engine/queue-helpers.mjs +161 -0
  243. package/src/tui/engine/session-stats.mjs +46 -0
  244. package/src/tui/engine/tool-call-fields.mjs +23 -0
  245. package/src/tui/engine/tool-result-text.mjs +126 -0
  246. package/src/tui/engine.mjs +569 -948
  247. package/src/tui/index.jsx +117 -7
  248. package/src/tui/input-editing.mjs +58 -8
  249. package/src/tui/input-editing.selection.test.mjs +75 -0
  250. package/src/tui/keyboard-protocol.mjs +42 -0
  251. package/src/tui/lib/voice-recorder.mjs +469 -0
  252. package/src/tui/markdown/format-token.mjs +128 -76
  253. package/src/tui/markdown/format-token.test.mjs +61 -19
  254. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  255. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  256. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  257. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  258. package/src/tui/markdown/table-layout.mjs +9 -9
  259. package/src/tui/paste-attachments.mjs +36 -9
  260. package/src/tui/paste-fix.test.mjs +119 -0
  261. package/src/tui/prompt-history-store.mjs +129 -0
  262. package/src/tui/prompt-history-store.test.mjs +52 -0
  263. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  264. package/src/tui/theme.mjs +41 -657
  265. package/src/tui/themes/base.mjs +86 -0
  266. package/src/tui/themes/basic.mjs +85 -0
  267. package/src/tui/themes/catppuccin.mjs +72 -0
  268. package/src/tui/themes/dracula.mjs +70 -0
  269. package/src/tui/themes/everforest.mjs +71 -0
  270. package/src/tui/themes/gruvbox.mjs +71 -0
  271. package/src/tui/themes/index.mjs +71 -0
  272. package/src/tui/themes/indigo.mjs +78 -0
  273. package/src/tui/themes/kanagawa.mjs +80 -0
  274. package/src/tui/themes/light.mjs +81 -0
  275. package/src/tui/themes/nord.mjs +72 -0
  276. package/src/tui/themes/onedark.mjs +16 -0
  277. package/src/tui/themes/rosepine.mjs +70 -0
  278. package/src/tui/themes/teal.mjs +80 -0
  279. package/src/tui/themes/tokyonight.mjs +79 -0
  280. package/src/tui/themes/utils.mjs +106 -0
  281. package/src/tui/themes/warm.mjs +79 -0
  282. package/src/tui/transcript-tool-failures.mjs +13 -2
  283. package/src/ui/markdown.mjs +1 -1
  284. package/src/ui/model-display.mjs +2 -2
  285. package/src/ui/statusline.mjs +75 -27
  286. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  287. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  288. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  289. package/src/workflows/default/WORKFLOW.md +46 -12
  290. package/src/workflows/sequential/WORKFLOW.md +51 -0
  291. package/src/workflows/solo/WORKFLOW.md +12 -1
  292. package/vendor/ink/build/display-width.js +62 -0
  293. package/vendor/ink/build/ink.js +100 -12
  294. package/vendor/ink/build/measure-text.js +4 -1
  295. package/vendor/ink/build/output.js +115 -9
  296. package/vendor/ink/build/render-node-to-output.js +4 -1
  297. package/vendor/ink/build/render.js +4 -0
  298. package/src/output-styles/extreme-simple.md +0 -20
  299. package/src/rules/lead/04-workflow.md +0 -51
  300. package/src/workflows/default/workflow.json +0 -13
  301. package/src/workflows/solo/workflow.json +0 -7
@@ -27,7 +27,7 @@
27
27
  // responds with OK_PARTIAL plus a hex-encoded failures payload that
28
28
  // the JS side surfaces per-entry.
29
29
 
30
- import { existsSync, readFileSync, realpathSync, statSync, mkdirSync } from 'node:fs';
30
+ import { existsSync, readFileSync, realpathSync, statSync, mkdirSync, writeFileSync } from 'node:fs';
31
31
  import { unlink } from 'node:fs/promises';
32
32
  import { spawn } from 'node:child_process';
33
33
  import { createInterface } from 'node:readline';
@@ -581,6 +581,41 @@ function isPatchErrorText(text) {
581
581
  return /^Error:/i.test(String(text ?? '').trimStart());
582
582
  }
583
583
 
584
+ // ── UI-only unified-diff side-channel ───────────────────────────────────────
585
+ // apply_patch's MODEL-VISIBLE result stays the compact summary ("Applied 1
586
+ // File (Native)\n OK Modify path — +1 Line · -1 Line"). The standard unified
587
+ // diff it builds internally (nativePatchStr) is stashed here, keyed by the
588
+ // tool_use id, so the agent loop can attach it to the TUI tool-result message
589
+ // as a UI-only field (never sent to the model). The TUI's expanded (ctrl+o)
590
+ // raw view then colorizes it via the existing formatExpandedResult/diff path.
591
+ // Size-capped per entry and FIFO-bounded so a huge patch (or a leaked entry
592
+ // whose take() never ran) cannot grow the transcript item or this Map.
593
+ const APPLY_PATCH_UI_DIFF_MAX_CHARS = 64 * 1024;
594
+ const APPLY_PATCH_UI_DIFF_REGISTRY_MAX = 64;
595
+ const _applyPatchUiDiffByCallId = new Map();
596
+
597
+ function registerApplyPatchUiDiff(callId, diff) {
598
+ if (!callId || typeof diff !== 'string' || !diff.trim()) return;
599
+ let text = diff;
600
+ if (text.length > APPLY_PATCH_UI_DIFF_MAX_CHARS) {
601
+ text = `${text.slice(0, APPLY_PATCH_UI_DIFF_MAX_CHARS)}\n… [diff truncated for display]`;
602
+ }
603
+ if (_applyPatchUiDiffByCallId.size >= APPLY_PATCH_UI_DIFF_REGISTRY_MAX) {
604
+ const oldest = _applyPatchUiDiffByCallId.keys().next().value;
605
+ if (oldest !== undefined) _applyPatchUiDiffByCallId.delete(oldest);
606
+ }
607
+ _applyPatchUiDiffByCallId.set(callId, text);
608
+ }
609
+
610
+ // Consume (read + remove) the UI-only unified diff for a tool_use id. Returns
611
+ // null when none was registered (error/rename-only/partial-fail/dry-skip).
612
+ export function takeApplyPatchUiDiff(callId) {
613
+ if (!callId) return null;
614
+ const value = _applyPatchUiDiffByCallId.get(callId) || null;
615
+ if (value != null) _applyPatchUiDiffByCallId.delete(callId);
616
+ return value;
617
+ }
618
+
584
619
  // Count how many source lines a hunk consumes vs produces so we can
585
620
  // surface a concise `lines_changed` figure without re-diffing.
586
621
  function countHunkChanges(hunks) {
@@ -1332,7 +1367,7 @@ function prepareInput(patchStr) {
1332
1367
  return String(patchStr).replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
1333
1368
  }
1334
1369
 
1335
- function isCodexApplyPatchEnvelope(patchStr) {
1370
+ function isApplyPatchEnvelope(patchStr) {
1336
1371
  const text = prepareInput(patchStr).trimStart();
1337
1372
  return text.startsWith('*** Begin Patch')
1338
1373
  || text.startsWith('*** Add File:')
@@ -1342,7 +1377,7 @@ function isCodexApplyPatchEnvelope(patchStr) {
1342
1377
 
1343
1378
  function isV4APatchInput(patchStr, format) {
1344
1379
  return String(format || '').toLowerCase() === 'v4a'
1345
- || isCodexApplyPatchEnvelope(patchStr);
1380
+ || isApplyPatchEnvelope(patchStr);
1346
1381
  }
1347
1382
 
1348
1383
  const UNIFIED_HUNK_HEADER_RE = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
@@ -2806,6 +2841,14 @@ async function apply_patch(args, cwd, options = {}) {
2806
2841
  if (renameLines.length > 0 && !isPatchErrorText(nativeResult)) {
2807
2842
  combined = `${renameLines.join('\n')}\n${nativeResult}`;
2808
2843
  }
2844
+ // UI-only: stash the standard unified diff (nativePatchStr) keyed by the
2845
+ // tool_use id so the agent loop can attach it to the TUI tool-result for the
2846
+ // expanded (ctrl+o) colored-diff view. NEVER added to the model-visible
2847
+ // result string. Only on clean success — partial/failed cases surface as an
2848
+ // Error card with the failure text instead.
2849
+ if (!isPatchErrorText(combined) && options?.toolCallId) {
2850
+ registerApplyPatchUiDiff(options.toolCallId, nativePatchStr);
2851
+ }
2809
2852
  if (!isPatchErrorText(combined) && rejectedV4AHunks.length > 0) {
2810
2853
  const tail = [
2811
2854
  '',
@@ -2824,6 +2867,74 @@ async function apply_patch(args, cwd, options = {}) {
2824
2867
  export const __patchTestHooks = { findFirstFailingUnifiedHunk, computeUnifiedChangeBand, collectUnifiedOps, unifiedOldLinesMatchAt, splitBufferLinesForPatch };
2825
2868
 
2826
2869
  export async function executePatchTool(name, args, cwd, options = {}) {
2870
+ // --- Opt-in failure replay capture -------------------------------------
2871
+ // When MIXDOG_PATCH_REPLAY_CAPTURE=1, a FAILED apply_patch (throw or
2872
+ // "Error:" body) is frozen to a replay file with the ORIGINAL patch args,
2873
+ // cwd, error, and a full snapshot of every target file's current bytes, so
2874
+ // scripts/patch-replay.mjs can re-run the exact failing call against updated
2875
+ // code. Local dev only; the patch body is stored verbatim (no redaction).
2876
+ return _executePatchTool(name, args, cwd, options);
2877
+ }
2878
+
2879
+ function patchReplayDir() {
2880
+ const base = process.env.MIXDOG_PATCH_REPLAY_DIR
2881
+ || pathJoin(getPluginDataDir(), 'history', 'patch-replays');
2882
+ return base;
2883
+ }
2884
+
2885
+ function getPluginDataDir() {
2886
+ try { return getPluginData(); } catch { /* fall through */ }
2887
+ return process.env.MIXDOG_DATA_DIR || pathJoin(process.env.USERPROFILE || process.env.HOME || '.', '.mixdog', 'data');
2888
+ }
2889
+
2890
+ function patchTargetPaths(patchStr, basePath) {
2891
+ const out = [];
2892
+ const re = /^\*\*\* (?:Update|Add|Delete) File:\s*(.+)$/gm;
2893
+ let m;
2894
+ while ((m = re.exec(String(patchStr || '')))) {
2895
+ const rel = m[1].trim();
2896
+ if (rel) out.push(rel);
2897
+ }
2898
+ // unified headers
2899
+ const ure = /^\+\+\+ (?:b\/)?(.+)$/gm;
2900
+ while ((m = ure.exec(String(patchStr || '')))) {
2901
+ const rel = m[1].trim();
2902
+ if (rel && rel !== '/dev/null') out.push(rel);
2903
+ }
2904
+ return [...new Set(out)];
2905
+ }
2906
+
2907
+ function maybeCapturePatchReplay(args, cwd, errorText) {
2908
+ if (process.env.MIXDOG_PATCH_REPLAY_CAPTURE !== '1') return;
2909
+ try {
2910
+ const patchStr = typeof args?.patch === 'string' ? args.patch : '';
2911
+ const basePath = pathResolve(String(args?.base_path || cwd || process.cwd()));
2912
+ const dir = patchReplayDir();
2913
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
2914
+ const rels = patchTargetPaths(patchStr, basePath);
2915
+ const files = {};
2916
+ for (const rel of rels) {
2917
+ try {
2918
+ const abs = isAbsolute(rel) ? rel : pathResolve(basePath, rel);
2919
+ files[rel] = existsSync(abs) ? readFileSync(abs, 'utf8') : null;
2920
+ } catch { files[rel] = null; }
2921
+ }
2922
+ const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
2923
+ const record = {
2924
+ id,
2925
+ ts: Date.now(),
2926
+ tool: 'apply_patch',
2927
+ args: { patch: patchStr, base_path: args?.base_path ?? null, format: args?.format ?? null, dry_run: args?.dry_run ?? null, fuzzy: args?.fuzzy ?? null, reject_partial: args?.reject_partial ?? null },
2928
+ cwd: basePath,
2929
+ error_first_line: String(errorText || '').split('\n')[0].slice(0, 400),
2930
+ targets: rels,
2931
+ file_snapshots: files,
2932
+ };
2933
+ writeFileSync(pathJoin(dir, `${id}.json`), JSON.stringify(record, null, 2), { mode: 0o600 });
2934
+ } catch { /* capture is best-effort; never affect the tool result */ }
2935
+ }
2936
+
2937
+ async function _executePatchTool(name, args, cwd, options = {}) {
2827
2938
  const effectiveCwd = cwd || process.cwd();
2828
2939
  switch (name) {
2829
2940
  case 'apply_patch': {
@@ -2831,9 +2942,12 @@ export async function executePatchTool(name, args, cwd, options = {}) {
2831
2942
  try {
2832
2943
  result = await apply_patch(args || {}, effectiveCwd, options);
2833
2944
  } catch (err) {
2834
- return `Error: ${err?.message || String(err)}`;
2945
+ const errText = `Error: ${err?.message || String(err)}`;
2946
+ maybeCapturePatchReplay(args, effectiveCwd, errText);
2947
+ return errText;
2835
2948
  }
2836
- // completion progress (claude "Found N" parity). Best-effort, no-op
2949
+ if (isPatchErrorText(String(result))) maybeCapturePatchReplay(args, effectiveCwd, String(result));
2950
+ // ② completion progress ("Found N" summary line). Best-effort, no-op
2837
2951
  // when onProgress is absent (no progressToken). Never throws — only
2838
2952
  // emits on success (an "Error:" body is left to the tool result alone).
2839
2953
  if (typeof options?.onProgress === 'function') {
@@ -1,5 +1,4 @@
1
- // Per-tool live-status message builder (ported from a reference agent's progress
2
- // generalization / getActivityDescription). Dependency-light and
1
+ // Per-tool live-status message builder. Dependency-light and
3
2
  // side-effect-free: given a tool name + its raw args, return a short
4
3
  // human-readable "what's happening now" string. Used by the central dispatch
5
4
  // path to emit a single start-of-tool progress notification. Every tool gets a
@@ -53,8 +53,7 @@ const SHELL_OUTPUT_DISK_CAP = 100 * 1024 * 1024;
53
53
 
54
54
  // Background-task disk watchdog cadence. The size guard polls the spilled
55
55
  // stdout/stderr files every interval and SIGKILLs the child once the
56
- // combined size exceeds SHELL_OUTPUT_DISK_CAP. 5 s matches reference agent
57
- // upstream cadence — short enough that a runaway loop is caught within a
56
+ // combined size exceeds SHELL_OUTPUT_DISK_CAP short enough that a runaway loop is caught within a
58
57
  // few seconds, long enough that the stat overhead is negligible.
59
58
  const SIZE_WATCHDOG_INTERVAL_MS = 1_000;
60
59
 
@@ -8,8 +8,7 @@
8
8
  // pyenv / mise / asdf / direnv setup the user gets in their interactive
9
9
  // terminal — without paying a fresh login-shell startup on every call.
10
10
  //
11
- // Mirrors upstream shell snapshot pattern (reference ShellSnapshot.ts:413,
12
- // createAndSaveSnapshot). Simpler scope: bash and zsh only, no embedded
11
+ // Scope: bash and zsh only, no embedded
13
12
  // search-tool injection (mixdog ships its own grep/glob helpers).
14
13
 
15
14
  import { spawn } from 'node:child_process';
@@ -34,8 +33,7 @@ const _cache = new Map();
34
33
  // command. Cleared on process exit (process-scoped Set).
35
34
  const _failedShells = new Set();
36
35
 
37
- // Mirrors reference cleanupRegistry pattern (utils/
38
- // cleanupRegistry.ts + ShellSnapshot.ts:534-545). Snapshot files are
36
+ // Snapshot files are
39
37
  // session-scoped and must be unlinked on graceful shutdown — otherwise
40
38
  // they pile up forever (each rc-file mtime change creates a new file).
41
39
  const _activeSnapshots = new Set();
@@ -16,10 +16,11 @@ import {
16
16
  realpathSync
17
17
  } from "fs";
18
18
  import { join, sep } from "path";
19
- import { chunk } from "../lib/format.mjs";
19
+ import { createHash } from "crypto";
20
+ import { chunk, formatForDiscord, MAX_DISCORD_MESSAGE } from "../lib/format.mjs";
20
21
  import { withConfigLock } from "../lib/config-lock.mjs";
21
22
  import { readSection, updateSection } from "../../shared/config.mjs";
22
- const MAX_CHUNK_LIMIT = 2e3;
23
+ const MAX_CHUNK_LIMIT = MAX_DISCORD_MESSAGE;
23
24
  const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
24
25
  const RECENT_SENT_CAP = 200;
25
26
  function defaultAccess() {
@@ -52,6 +53,7 @@ function safeAttName(att) {
52
53
  }
53
54
  class DiscordBackend {
54
55
  name = "discord";
56
+ MAX_MESSAGE_LENGTH = MAX_DISCORD_MESSAGE;
55
57
  onMessage = null;
56
58
  onInteraction = null;
57
59
  onModalRequest = null;
@@ -77,6 +79,9 @@ class DiscordBackend {
77
79
  this.initialAccess = normalizeAccess(config.access);
78
80
  this.client = null;
79
81
  }
82
+ formatOutgoing(text) {
83
+ return formatForDiscord(text);
84
+ }
80
85
  // ── Lifecycle ──────────────────────────────────────────────────────
81
86
  async connect() {
82
87
  // Re-entry guard: if a connect() is already in-flight or completed, return
@@ -335,33 +340,118 @@ class DiscordBackend {
335
340
  }
336
341
  }
337
342
  if (files.length > 10) throw new Error("max 10 attachments per message");
338
- if (text && this.sendCount > 0) {
343
+ // Resume-token support (opaque to callers). A partial-send failure hands the
344
+ // caller a token { hash, nextChunkIdx, sentIds, prefixed }; a requeued retry
345
+ // passes it back so we resume at the failed chunk instead of re-sending
346
+ // chunks that already landed.
347
+ //
348
+ // The "\u3164\n" prefix is applied from this.sendCount, which advances when
349
+ // ANY backend send fully succeeds. Between the original partial failure and
350
+ // this retry, an unrelated send (scheduler/lifecycle/permission) can flip
351
+ // sendCount from 0 to >0, which would change the prefix — and therefore the
352
+ // chunked text and its hash — breaking the token match and forcing a
353
+ // duplicate full-resend. To keep the retry byte-identical to the original
354
+ // attempt, freeze the prefix decision in the token and reuse it here instead
355
+ // of recomputing from the (possibly changed) current sendCount.
356
+ const resumeToken = opts?.resumeToken;
357
+ const applyPrefix = (resumeToken && typeof resumeToken.prefixed === "boolean")
358
+ ? resumeToken.prefixed
359
+ : (text && this.sendCount > 0 ? true : false);
360
+ if (text && applyPrefix) {
339
361
  text = "\u3164\n" + text;
340
362
  }
341
363
  const access = this.loadAccess();
342
364
  const limit = Math.max(1, Math.min(access.textChunkLimit ?? MAX_CHUNK_LIMIT, MAX_CHUNK_LIMIT));
343
365
  const replyMode = access.replyToMode ?? "off";
344
366
  const chunks = chunk(text, limit);
367
+ // Hash the FINAL prefixed text so the token pins to the exact bytes sent.
368
+ const contentHash = createHash("md5").update(text).digest("hex");
369
+ let startIdx = 0;
345
370
  const sentIds = [];
371
+ // Resume only when BOTH the text hash AND the effective chunk limit match.
372
+ // The token's nextChunkIdx is an index into the chunk array, which is only
373
+ // meaningful for the same `limit`; if access.textChunkLimit changed during
374
+ // the retry window the same text can split differently, so a stale index
375
+ // could skip or duplicate a range. On mismatch fall through to startIdx=0
376
+ // (full, safe resend).
377
+ if (resumeToken && resumeToken.hash === contentHash && resumeToken.limit === limit) {
378
+ startIdx = Math.max(0, Math.min(resumeToken.nextChunkIdx ?? 0, chunks.length));
379
+ // Seed sentIds with the already-delivered ids so the returned list stays
380
+ // complete across the resume boundary.
381
+ if (Array.isArray(resumeToken.sentIds)) sentIds.push(...resumeToken.sentIds);
382
+ }
346
383
  try {
347
- for (let i = 0; i < chunks.length; i++) {
384
+ for (let i = startIdx; i < chunks.length; i++) {
348
385
  const shouldReplyTo = replyTo != null && replyMode !== "off" && (replyMode === "all" || i === 0);
349
386
  const embeds = i === 0 ? opts?.embeds ?? [] : [];
350
387
  const components = i === 0 ? opts?.components ?? [] : [];
351
- const sent = await ch.send({
388
+ const payload = {
352
389
  content: chunks[i],
353
390
  ...embeds.length > 0 ? { embeds } : {},
354
391
  ...components.length > 0 ? { components } : {},
355
392
  ...i === 0 && files.length > 0 ? { files } : {},
356
393
  ...shouldReplyTo ? { reply: { messageReference: replyTo, failIfNotExists: false } } : {}
357
- });
358
- this.noteSent(sent.id);
359
- sentIds.push(sent.id);
394
+ };
395
+ // Per-chunk send with retry. On 429 honour Retry-After; on other
396
+ // transient errors do a short backoff. Cap at 3 attempts per chunk;
397
+ // after the cap throw an error carrying a resume token so the caller
398
+ // (output-forwarder) requeues the WHOLE item and the next sendMessage
399
+ // resumes at this chunk index instead of re-sending 0..i-1.
400
+ let attempt = 0;
401
+ for (;;) {
402
+ try {
403
+ const sent = await ch.send(payload);
404
+ this.noteSent(sent.id);
405
+ sentIds.push(sent.id);
406
+ break;
407
+ } catch (err) {
408
+ attempt++;
409
+ const status = err?.status ?? err?.code ?? err?.httpStatus;
410
+ // Classify: PERMANENT = a 4xx client error (unknown channel/message,
411
+ // missing access/permissions, 404 …) that will never succeed on
412
+ // retry. TRANSIENT = 429, any 5xx, or network error (no status). A
413
+ // permanent error must NOT retry-loop and must NOT carry a resume
414
+ // token — throw it immediately flagged so the forwarder drops the
415
+ // item instead of requeuing forever.
416
+ const isPermanent = typeof status === "number" && status >= 400 && status < 500 && status !== 429;
417
+ if (isPermanent) {
418
+ const e = err instanceof Error ? err : new Error(String(err));
419
+ e.permanent = true;
420
+ throw e;
421
+ }
422
+ if (attempt >= 3) {
423
+ // Attach an opaque resume token pointing at the chunk that failed
424
+ // (i). sentIds holds every chunk already delivered (including any
425
+ // seeded from a prior token).
426
+ const e = err instanceof Error ? err : new Error(String(err));
427
+ e.resumeToken = { hash: contentHash, nextChunkIdx: i, sentIds: [...sentIds], prefixed: applyPrefix, limit };
428
+ throw e;
429
+ }
430
+ if (status === 429) {
431
+ // @discordjs/rest RateLimitError.retryAfter is ALWAYS in ms
432
+ // (no unit guess). Clamp to [0, 60000]; fall back to 1s when the
433
+ // field is absent/invalid. (discord.js also auto-sleeps rate
434
+ // limits internally, so we rarely even reach this branch.)
435
+ const retryAfterMs = Number(err?.retryAfter);
436
+ const ms = Number.isFinite(retryAfterMs) && retryAfterMs > 0 ? Math.min(retryAfterMs, 60_000) : 1000;
437
+ await new Promise((r) => setTimeout(r, ms));
438
+ } else {
439
+ // Other transient error (5xx / network): short backoff.
440
+ await new Promise((r) => setTimeout(r, 1000));
441
+ }
442
+ }
443
+ }
360
444
  }
361
445
  this.sendCount += sentIds.length;
362
446
  } catch (err) {
363
447
  const msg = err instanceof Error ? err.message : String(err);
364
- throw new Error(`send failed after ${sentIds.length}/${chunks.length} chunk(s): ${msg}`);
448
+ const wrapped = new Error(`send failed after ${sentIds.length}/${chunks.length} chunk(s): ${msg}`);
449
+ // Preserve the opaque resume token across the rewrap so the caller can
450
+ // persist it on the queue item and resume on retry.
451
+ if (err?.resumeToken) wrapped.resumeToken = err.resumeToken;
452
+ // Propagate the permanent flag so the forwarder drops (not requeues) it.
453
+ if (err?.permanent) wrapped.permanent = true;
454
+ throw wrapped;
365
455
  }
366
456
  return { sentIds };
367
457
  }