mixdog 0.9.95 → 0.9.97

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 (282) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/README.md +0 -17
  6. package/package.json +16 -12
  7. package/scripts/.tmp-coverage-report.mjs +96 -0
  8. package/scripts/build-tui.mjs +17 -1
  9. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  10. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  11. package/src/defaults/mixdog-config.template.json +5 -8
  12. package/src/defaults/skills/setup/SKILL.md +1 -1
  13. package/src/lib/keychain-cjs.cjs +1 -1
  14. package/src/lib/mixdog-debug.cjs +2 -3
  15. package/src/rules/agent/30-explorer.md +7 -4
  16. package/src/rules/lead/01-general.md +1 -2
  17. package/src/rules/lead/lead-tool.md +0 -3
  18. package/src/rules/shared/01-tool.md +21 -29
  19. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  23. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  24. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  25. package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
  26. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  27. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  36. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  39. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  41. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  42. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  43. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
  44. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
  46. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  48. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  49. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
  52. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  53. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  54. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  55. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  57. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  58. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  59. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  61. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  62. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  63. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  64. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
  66. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  67. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
  71. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  72. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  73. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  74. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  77. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
  81. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  84. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  85. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  86. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  87. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  88. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  89. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  90. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
  91. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  92. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
  93. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
  94. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  95. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  97. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
  99. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  101. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
  102. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
  104. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  105. package/src/runtime/channels/lib/config.mjs +34 -34
  106. package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
  107. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  108. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  109. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  110. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  111. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  112. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  113. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  114. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  115. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  116. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  117. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  118. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  119. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  120. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  121. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  122. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  123. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/memory/index.mjs +76 -248
  126. package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
  127. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  128. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  129. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
  130. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  131. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  132. package/src/runtime/memory/lib/http-router.mjs +9 -6
  133. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  134. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  135. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  136. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  137. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  138. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  139. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  140. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  141. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  142. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  143. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  144. package/src/runtime/memory/lib/memory.mjs +1 -1
  145. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  146. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  147. package/src/runtime/memory/tool-defs.mjs +1 -5
  148. package/src/runtime/search/index.mjs +2 -2
  149. package/src/runtime/search/tool-defs.mjs +2 -2
  150. package/src/runtime/shared/abort-race.mjs +70 -0
  151. package/src/runtime/shared/agent-route-config.mjs +119 -0
  152. package/src/runtime/shared/atomic-file.mjs +80 -4
  153. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  154. package/src/runtime/shared/child-guardian.mjs +204 -108
  155. package/src/runtime/shared/child-spawn-gate.mjs +139 -72
  156. package/src/runtime/shared/config.mjs +120 -47
  157. package/src/runtime/shared/llm/index.mjs +1 -1
  158. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  159. package/src/runtime/shared/resource-admission.mjs +150 -100
  160. package/src/runtime/shared/stream-progress.mjs +6 -0
  161. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  162. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  163. package/src/runtime/shared/tool-surface.mjs +91 -27
  164. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  165. package/src/runtime/shared/turn-snapshot.mjs +47 -6
  166. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  167. package/src/session-runtime/channel-config-api.mjs +5 -5
  168. package/src/session-runtime/config-helpers.mjs +39 -37
  169. package/src/session-runtime/config-lifecycle.mjs +27 -27
  170. package/src/session-runtime/lifecycle-api.mjs +22 -3
  171. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  172. package/src/session-runtime/model-route-api.mjs +7 -5
  173. package/src/session-runtime/prewarm.mjs +1 -1
  174. package/src/session-runtime/provider-auth-api.mjs +0 -7
  175. package/src/session-runtime/provider-models.mjs +1 -1
  176. package/src/session-runtime/remote-control.mjs +6 -7
  177. package/src/session-runtime/runtime-core.mjs +63 -39
  178. package/src/session-runtime/runtime-tunables.mjs +4 -4
  179. package/src/session-runtime/self-update.mjs +1 -1
  180. package/src/session-runtime/session-lifecycle.mjs +45 -17
  181. package/src/session-runtime/session-title.mjs +1 -1
  182. package/src/session-runtime/session-turn-api.mjs +96 -24
  183. package/src/session-runtime/settings-api.mjs +12 -24
  184. package/src/session-runtime/tool-defs.mjs +3 -3
  185. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  186. package/src/session-runtime/workflow.mjs +14 -24
  187. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  188. package/src/standalone/agent-tool/helpers.mjs +2 -2
  189. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  190. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  191. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  192. package/src/standalone/channel-admin.mjs +53 -64
  193. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
  194. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
  195. package/src/standalone/channel-worker.mjs +124 -647
  196. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
  197. package/src/standalone/explore-tool.mjs +187 -70
  198. package/src/standalone/fair-call-scheduler.mjs +264 -0
  199. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  200. package/src/standalone/session-client.mjs +1134 -0
  201. package/src/standalone/session-protocol.mjs +192 -0
  202. package/src/standalone/session-runtime-pool.mjs +393 -0
  203. package/src/standalone/session-runtime-record.mjs +20 -0
  204. package/src/standalone/session-runtime-worker.mjs +203 -0
  205. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
  206. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
  207. package/src/standalone/session-wire.mjs +55 -0
  208. package/src/tui/App.jsx +34 -21
  209. package/src/tui/app/app-format.mjs +1 -1
  210. package/src/tui/app/app-view.jsx +5 -2
  211. package/src/tui/app/channel-pickers.mjs +41 -41
  212. package/src/tui/app/core-memory-picker.mjs +19 -19
  213. package/src/tui/app/doctor.mjs +5 -11
  214. package/src/tui/app/input-parsers.mjs +2 -2
  215. package/src/tui/app/message-selector.mjs +103 -0
  216. package/src/tui/app/onboarding-steps.mjs +3 -4
  217. package/src/tui/app/project-picker.mjs +80 -56
  218. package/src/tui/app/prompt-submit.mjs +48 -43
  219. package/src/tui/app/resume-picker.mjs +2 -2
  220. package/src/tui/app/route-pickers.mjs +3 -1
  221. package/src/tui/app/settings-picker.mjs +31 -33
  222. package/src/tui/app/transcript-window.mjs +38 -0
  223. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  224. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  225. package/src/tui/app/use-prompt-hint.mjs +3 -2
  226. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  227. package/src/tui/app/use-transcript-scroll.mjs +10 -3
  228. package/src/tui/app/use-transcript-window.mjs +50 -17
  229. package/src/tui/components/PromptInput.jsx +71 -38
  230. package/src/tui/components/Spinner.jsx +1 -1
  231. package/src/tui/components/ToolExecution.jsx +1 -1
  232. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  233. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  234. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  235. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  236. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  237. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  238. package/src/tui/dist/index.mjs +1031 -1676
  239. package/src/tui/hooks/useSession.mjs +16 -0
  240. package/src/tui/index.jsx +12 -13
  241. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  242. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  243. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  244. package/src/tui/{engine → session}/context-state.mjs +3 -3
  245. package/src/tui/{engine → session}/labels.mjs +2 -2
  246. package/src/tui/{engine → session}/live-share.mjs +38 -4
  247. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  248. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  249. package/src/tui/session/oauth-flows.mjs +140 -0
  250. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  251. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  252. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  253. package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
  254. package/src/tui/{engine → session}/session-api.mjs +110 -93
  255. package/src/tui/{engine → session}/session-flow.mjs +8 -2
  256. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  257. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  258. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  259. package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
  260. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  261. package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
  262. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  263. package/src/tui/{engine → session}/turn.mjs +38 -176
  264. package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
  265. package/src/tui/session.mjs +22 -0
  266. package/src/tui/spinner-meta.mjs +4 -4
  267. package/src/tui/spinner-verbs.mjs +51 -3
  268. package/src/ui/statusline-segments.mjs +1 -1
  269. package/src/ui/statusline.mjs +1 -1
  270. package/vendor/ink/build/components/App.js +10 -1
  271. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  272. package/src/standalone/engine-daemon-client.mjs +0 -894
  273. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
  274. package/src/standalone/engine-daemon-protocol.mjs +0 -33
  275. package/src/tui/engine/turn-watchdog.mjs +0 -92
  276. package/src/tui/engine.mjs +0 -22
  277. package/src/tui/hooks/useEngine.mjs +0 -16
  278. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  279. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  280. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  281. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  282. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -0,0 +1,291 @@
1
+ import { availableParallelism } from 'node:os';
2
+ import { Worker } from 'node:worker_threads';
3
+ import { currentProviderAdmissionOwner } from './admission-scheduler.mjs';
4
+
5
+ const DEFAULT_MIN_BATCH_BYTES = 32 * 1024;
6
+
7
+ function positiveInt(value, fallback) {
8
+ const parsed = Math.floor(Number(value));
9
+ return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
10
+ }
11
+
12
+ function configuredWorkerCount(env = process.env) {
13
+ const configured = Number(env.MIXDOG_PROVIDER_STREAM_WORKERS);
14
+ if (configured === 0) return 0;
15
+ if (Number.isFinite(configured) && configured > 0) return Math.floor(configured);
16
+ return Math.max(1, Math.min(4, availableParallelism() - 1));
17
+ }
18
+
19
+ function abortError(signal) {
20
+ return signal?.reason instanceof Error
21
+ ? signal.reason
22
+ : new Error(String(signal?.reason || 'provider stream parse canceled'));
23
+ }
24
+
25
+ function syntaxError(details) {
26
+ const error = new SyntaxError(String(details?.message || 'invalid JSON'));
27
+ error.name = String(details?.name || 'SyntaxError');
28
+ return error;
29
+ }
30
+
31
+ /**
32
+ * Reusable worker pool for the CPU part of provider SSE handling.
33
+ *
34
+ * Small deltas stay inline because a Worker round-trip costs more than parsing
35
+ * them. Large events and multi-event network chunks are parsed off the daemon
36
+ * event loop. Calls are never admission-capped: every request is posted
37
+ * immediately and workers consume their independent message queues.
38
+ */
39
+ export function createStreamJsonPool({
40
+ maxWorkers = configuredWorkerCount(),
41
+ minBatchBytes = Number(process.env.MIXDOG_PROVIDER_STREAM_WORKER_MIN_BYTES)
42
+ || DEFAULT_MIN_BATCH_BYTES,
43
+ maxPendingBytes = (Number(process.env.MIXDOG_PROVIDER_STREAM_PENDING_MB) || 32)
44
+ * 1024 * 1024,
45
+ WorkerImpl = Worker,
46
+ } = {}) {
47
+ const workerMax = Math.max(0, Math.floor(Number(maxWorkers) || 0));
48
+ const inlineBelowBytes = Math.max(0, Math.floor(Number(minBatchBytes) || 0));
49
+ const slots = [];
50
+ const ownerAffinities = new Map();
51
+ const waiting = [];
52
+ const pendingByteMax = Math.max(1024 * 1024, Math.floor(Number(maxPendingBytes) || 0));
53
+ let pendingBytes = 0;
54
+ let waitingBytes = 0;
55
+ let sequence = 0;
56
+ let closed = false;
57
+ const stats = {
58
+ inlineBatches: 0,
59
+ offloadedBatches: 0,
60
+ fallbackBatches: 0,
61
+ parsedBytes: 0,
62
+ };
63
+
64
+ function parseInline(payloads) {
65
+ return payloads.map((payload) => JSON.parse(payload));
66
+ }
67
+
68
+ function removeSlot(slot) {
69
+ const index = slots.indexOf(slot);
70
+ if (index >= 0) slots.splice(index, 1);
71
+ for (const [owner, assigned] of ownerAffinities) {
72
+ if (assigned === slot) ownerAffinities.delete(owner);
73
+ }
74
+ }
75
+
76
+ function failSlot(slot) {
77
+ if (slot.failed) return;
78
+ slot.failed = true;
79
+ removeSlot(slot);
80
+ for (const task of slot.tasks.values()) {
81
+ pendingBytes = Math.max(0, pendingBytes - task.bytes);
82
+ task.detach();
83
+ if (task.aborted) continue;
84
+ stats.fallbackBatches += 1;
85
+ try { task.resolve(parseInline(task.payloads)); }
86
+ catch (error) { task.reject(error); }
87
+ }
88
+ slot.tasks.clear();
89
+ try { slot.worker.terminate(); } catch {}
90
+ drainWaiting();
91
+ }
92
+
93
+ function createSlot() {
94
+ const worker = new WorkerImpl(new URL('./stream-json-worker.mjs', import.meta.url), {
95
+ execArgv: [],
96
+ });
97
+ worker.unref?.();
98
+ const slot = { worker, tasks: new Map(), failed: false };
99
+ worker.on('message', (message) => {
100
+ const task = slot.tasks.get(Number(message?.id));
101
+ if (!task) return;
102
+ slot.tasks.delete(Number(message.id));
103
+ pendingBytes = Math.max(0, pendingBytes - task.bytes);
104
+ task.detach();
105
+ if (task.aborted) return;
106
+ if (message?.ok === true) task.resolve(message.values);
107
+ else task.reject(syntaxError(message?.error));
108
+ drainWaiting();
109
+ });
110
+ worker.on('error', () => failSlot(slot));
111
+ worker.on('exit', () => {
112
+ if (!closed) failSlot(slot);
113
+ else removeSlot(slot);
114
+ });
115
+ slots.push(slot);
116
+ return slot;
117
+ }
118
+
119
+ function pickSlot(ownerKey) {
120
+ const ready = slots.filter((slot) => !slot.failed);
121
+ const owner = String(ownerKey || '').trim().slice(0, 240);
122
+ const assigned = owner ? ownerAffinities.get(owner) : null;
123
+ if (assigned && !assigned.failed) return assigned;
124
+ const least = ready.sort((left, right) => left.tasks.size - right.tasks.size)[0] || null;
125
+ const slot = ready.length < workerMax && (!least || least.tasks.size > 0)
126
+ ? createSlot()
127
+ : least || createSlot();
128
+ if (owner) {
129
+ ownerAffinities.set(owner, slot);
130
+ while (ownerAffinities.size > 4096) {
131
+ ownerAffinities.delete(ownerAffinities.keys().next().value);
132
+ }
133
+ }
134
+ return slot;
135
+ }
136
+
137
+ function removeWaiting(task) {
138
+ const index = waiting.indexOf(task);
139
+ if (index < 0) return false;
140
+ waiting.splice(index, 1);
141
+ waitingBytes = Math.max(0, waitingBytes - task.bytes);
142
+ return true;
143
+ }
144
+
145
+ function postTask(task) {
146
+ const slot = pickSlot(task.ownerKey);
147
+ task.slot = slot;
148
+ slot.tasks.set(task.id, task);
149
+ pendingBytes += task.bytes;
150
+ try {
151
+ slot.worker.postMessage({ id: task.id, payloads: task.payloads });
152
+ } catch {
153
+ slot.tasks.delete(task.id);
154
+ pendingBytes = Math.max(0, pendingBytes - task.bytes);
155
+ task.detach();
156
+ stats.fallbackBatches += 1;
157
+ try { task.resolve(parseInline(task.payloads)); }
158
+ catch (error) { task.reject(error); }
159
+ drainWaiting();
160
+ }
161
+ }
162
+
163
+ function drainWaiting() {
164
+ while (waiting.length > 0) {
165
+ const task = waiting[0];
166
+ if (pendingBytes > 0 && pendingBytes + task.bytes > pendingByteMax) return;
167
+ waiting.shift();
168
+ waitingBytes = Math.max(0, waitingBytes - task.bytes);
169
+ if (task.aborted) continue;
170
+ postTask(task);
171
+ }
172
+ }
173
+
174
+ function parseBatch(values, {
175
+ signal = null,
176
+ ownerKey = currentProviderAdmissionOwner(),
177
+ } = {}) {
178
+ const payloads = (Array.isArray(values) ? values : [values]).map((value) => String(value));
179
+ if (signal?.aborted) return Promise.reject(abortError(signal));
180
+ const bytes = payloads.reduce((sum, payload) => sum + Buffer.byteLength(payload), 0);
181
+ stats.parsedBytes += bytes;
182
+ if (workerMax === 0 || bytes < inlineBelowBytes) {
183
+ stats.inlineBatches += 1;
184
+ try { return Promise.resolve(parseInline(payloads)); }
185
+ catch (error) { return Promise.reject(error); }
186
+ }
187
+ if (closed) return Promise.reject(new Error('provider stream JSON pool is closed'));
188
+ if (bytes > pendingByteMax) {
189
+ const error = new Error(
190
+ `provider stream JSON batch exceeded ${pendingByteMax} bytes`,
191
+ );
192
+ error.code = 'ERESOURCEPRESSURE';
193
+ return Promise.reject(error);
194
+ }
195
+
196
+ const id = ++sequence;
197
+ stats.offloadedBatches += 1;
198
+ return new Promise((resolve, reject) => {
199
+ const task = {
200
+ id,
201
+ ownerKey,
202
+ bytes,
203
+ payloads,
204
+ resolve,
205
+ reject,
206
+ aborted: false,
207
+ onAbort: null,
208
+ detach() {
209
+ if (!task.onAbort || !signal) return;
210
+ try { signal.removeEventListener('abort', task.onAbort); } catch {}
211
+ task.onAbort = null;
212
+ },
213
+ };
214
+ if (signal) {
215
+ task.onAbort = () => {
216
+ if (task.aborted) return;
217
+ task.aborted = true;
218
+ task.detach();
219
+ if (removeWaiting(task)) {
220
+ reject(abortError(signal));
221
+ return;
222
+ }
223
+ reject(abortError(signal));
224
+ };
225
+ signal.addEventListener('abort', task.onAbort, { once: true });
226
+ }
227
+ if (pendingBytes > 0 && pendingBytes + bytes > pendingByteMax) {
228
+ if (waitingBytes + bytes > pendingByteMax) {
229
+ task.detach();
230
+ const error = new Error(
231
+ `provider stream JSON backlog exceeded ${pendingByteMax} bytes`,
232
+ );
233
+ error.code = 'ERESOURCEPRESSURE';
234
+ reject(error);
235
+ return;
236
+ }
237
+ waiting.push(task);
238
+ waitingBytes += bytes;
239
+ return;
240
+ }
241
+ postTask(task);
242
+ });
243
+ }
244
+
245
+ async function close(reason = 'provider stream JSON pool closed') {
246
+ if (closed) return;
247
+ closed = true;
248
+ const error = new Error(reason);
249
+ for (const task of waiting.splice(0)) {
250
+ task.detach();
251
+ if (!task.aborted) task.reject(error);
252
+ }
253
+ waitingBytes = 0;
254
+ const workers = [];
255
+ for (const slot of slots.splice(0)) {
256
+ for (const task of slot.tasks.values()) {
257
+ task.detach();
258
+ if (!task.aborted) task.reject(error);
259
+ }
260
+ slot.tasks.clear();
261
+ workers.push(Promise.resolve(slot.worker.terminate()).catch(() => {}));
262
+ }
263
+ ownerAffinities.clear();
264
+ await Promise.all(workers);
265
+ }
266
+
267
+ function snapshot() {
268
+ return {
269
+ workers: slots.filter((slot) => !slot.failed).length,
270
+ workerMax,
271
+ activeBatches: slots.reduce((sum, slot) => sum + slot.tasks.size, 0),
272
+ pendingBytes,
273
+ waitingBatches: waiting.length,
274
+ waitingBytes,
275
+ maxPendingBytes: pendingByteMax,
276
+ ownerAffinities: ownerAffinities.size,
277
+ inlineBatches: stats.inlineBatches,
278
+ offloadedBatches: stats.offloadedBatches,
279
+ fallbackBatches: stats.fallbackBatches,
280
+ parsedBytes: stats.parsedBytes,
281
+ };
282
+ }
283
+
284
+ return { parseBatch, close, snapshot };
285
+ }
286
+
287
+ export const providerStreamJsonPool = createStreamJsonPool();
288
+ export const parseProviderJsonBatch = (payloads, options) =>
289
+ providerStreamJsonPool.parseBatch(payloads, options);
290
+ export const providerStreamJsonSnapshot = () => providerStreamJsonPool.snapshot();
291
+ export const closeProviderStreamJsonPool = (reason) => providerStreamJsonPool.close(reason);
@@ -0,0 +1,21 @@
1
+ import { parentPort } from 'node:worker_threads';
2
+
3
+ if (!parentPort) throw new Error('provider stream JSON worker requires parentPort');
4
+
5
+ parentPort.on('message', (message) => {
6
+ const id = Number(message?.id);
7
+ const payloads = Array.isArray(message?.payloads) ? message.payloads : [];
8
+ try {
9
+ const values = payloads.map((payload) => JSON.parse(String(payload)));
10
+ parentPort.postMessage({ id, ok: true, values });
11
+ } catch (error) {
12
+ parentPort.postMessage({
13
+ id,
14
+ ok: false,
15
+ error: {
16
+ name: String(error?.name || 'SyntaxError'),
17
+ message: String(error?.message || error || 'invalid JSON'),
18
+ },
19
+ });
20
+ }
21
+ });
@@ -30,7 +30,7 @@ import {
30
30
  } from './loop/tool-classify.mjs';
31
31
  import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
32
32
  import { runRecallFastTrackCompact } from './loop/recall-fasttrack.mjs';
33
- import { executeTool, _scopedCacheOutcomeForCall } from './loop/tool-exec.mjs';
33
+ import { executeTool, _scopedCacheOutcomeForCall, resolveLiveToolCwd } from './loop/tool-exec.mjs';
34
34
 
35
35
  // classifyResultKind is imported from result-classification.mjs at the top of
36
36
  // this file; import it from there directly rather than via this module.
@@ -403,23 +403,26 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
403
403
  // Continuations since the last executed tool batch — bounds the text-only
404
404
  // continuation runaway (see PROVIDER_CONTINUATION_NO_TOOL_LIMIT).
405
405
  let _continuationsSinceToolBatch = 0;
406
- // Loop-level transport replays consumed this ask (see send-with-recovery
407
- // TRANSPORT_RETRY_MAX): bounded per turn, reset only with a fresh ask.
406
+ // Loop-level transport replays consumed since the last SUCCESSFUL send
407
+ // (see send-with-recovery TRANSPORT_RETRY_MAX). Reference parity: codex
408
+ // starts each sampling request at retries=0 (and resets on transport
409
+ // fallback), cc's withRetry budget is per request, opencode schedules
410
+ // retries per stream call. A per-ask budget instead let one early blip in
411
+ // a long turn leave every later iteration with zero replays.
408
412
  let _transportRetriesUsed = 0;
409
413
  // Queued prompt/task notifications are attached after a
410
414
  // tool batch, before the continuation provider send. Normal batches drain
411
415
  // up to 'next'; a Sleep-like tool grants a 'later' flush.
412
416
  let _toolBatchJustCompleted = false;
417
+ let _lastToolBatchEndedAt = 0;
413
418
  let _lastToolBatchHadSleep = false;
414
419
  const isSleepLikeToolCall = (call) => {
415
420
  const name = String(call?.name || call?.toolName || call?.function?.name || '').toLowerCase();
416
421
  return name === 'sleep' || name.endsWith('/sleep') || name.endsWith('.sleep');
417
422
  };
418
- // Tool execution must use the session cwd even when the caller omitted the
419
- // legacy positional cwd argument. Agent workers always carry their cwd on
420
- // sessionRef; falling through to pwd()/process.cwd() resolves relatives
421
- // against the host/plugin root instead of the worker workspace.
422
- cwd = cwd || sessionRef?.cwd || undefined;
423
+ // sessionRef.cwd is the live SSOT. The legacy positional cwd is only the
424
+ // turn-start snapshot and becomes stale after an in-turn cwd tool call.
425
+ cwd = resolveLiveToolCwd(cwd, sessionRef);
423
426
  // Staged pre-cap warnings + one true hard stop. The ONLY count-based
424
427
  // forced termination is the hard cap at maxLoopIterations (default 200):
425
428
  // a genuine runaway guard. Before it, staged warnings fire at 50%/75%/90%
@@ -440,6 +443,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
440
443
  Math.floor(maxLoopIterations * 0.9),
441
444
  ];
442
445
  while (true) {
446
+ // A cwd tool call updates sessionRef in place. Refresh before building
447
+ // this iteration's eager dispatcher and cache keys so every following
448
+ // tool family, including apply_patch, observes the new write root.
449
+ cwd = resolveLiveToolCwd(cwd, sessionRef);
443
450
  const _iterT0 = Date.now();
444
451
  throwIfAborted();
445
452
  if (iterations >= maxLoopIterations) {
@@ -457,7 +464,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
457
464
  // non-empty final so callers never see an empty response.
458
465
  if (response && !String(response.content || '').trim()) {
459
466
  response.content = sessionAgent === 'explorer'
460
- ? 'EXPLORATION_FAILED'
467
+ ? 'EXPLORATION_FAILED [iteration-cap] — explorer hit its tool-turn cap with zero anchors; retry once with changed tokens or a narrower root'
461
468
  : '[iteration cap reached before final text]';
462
469
  if (Array.isArray(response.toolCalls)) response.toolCalls = [];
463
470
  }
@@ -632,6 +639,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
632
639
  return result;
633
640
  };
634
641
  const sendStartedAt = Date.now();
642
+ const preSendMs = sendStartedAt - _iterT0;
643
+ const toolResumeMs = _lastToolBatchEndedAt
644
+ ? sendStartedAt - _lastToolBatchEndedAt
645
+ : null;
635
646
  try { opts.onProviderSendStarted?.(); } catch {}
636
647
  const _sendResult = await runWithProviderRequestToolsScope(
637
648
  requestToolScope,
@@ -659,6 +670,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
659
670
  opts.onToolCall = undefined;
660
671
  delete opts.cacheBreakIntent;
661
672
  contextOverflowRetryUsed = false;
673
+ // A completed send ends the outage this budget was covering; the next
674
+ // iteration is a fresh request and must get the full replay budget
675
+ // again (mirrors contextOverflowRetryUsed above).
676
+ _transportRetriesUsed = 0;
662
677
  // Capture opaque state for the next turn only when the provider
663
678
  // explicitly returned the field. Absence means "no update"; an own
664
679
  // property with null/undefined means "clear".
@@ -679,6 +694,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
679
694
  sessionId,
680
695
  iteration: iterations,
681
696
  sendMs: Date.now() - sendStartedAt,
697
+ preSendMs,
698
+ toolResumeMs,
682
699
  messageCount: Array.isArray(messages) ? messages.length : 0,
683
700
  bodyBytesEst: _traceVerbose
684
701
  ? estimateProviderPayloadBytes(messages, model, sendTools)
@@ -1050,15 +1067,24 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
1050
1067
  }));
1051
1068
  // Settle the stop hook on the batch that actually executed.
1052
1069
  _toolFailureStopHook.endBatch(_callsToExecute);
1070
+ const _toolsEndedAt = Date.now();
1071
+ try {
1072
+ opts.onToolPhaseCompleted?.({
1073
+ iteration: nextIteration,
1074
+ calls: _callsToExecute.length,
1075
+ elapsedMs: _toolsEndedAt - _toolsT0,
1076
+ });
1077
+ } catch {}
1053
1078
  // Loop-phase timing (diagnostics): where non-model time goes per
1054
1079
  // iteration — presend (repair/compact/snapshot), send (provider
1055
1080
  // round-trip incl. streaming), tools (batch execution). Gated by the
1056
1081
  // same env as [turn-timing] so bench runs opt in via -AgentEnv.
1057
1082
  if (process.env.MIXDOG_TURN_TIMING === '1') {
1058
1083
  try {
1059
- process.stderr.write(`[loop-timing] iter=${nextIteration} presend=${sendStartedAt - _iterT0}ms send=${_sendEndedAt - sendStartedAt}ms tools=${Date.now() - _toolsT0}ms calls=${_callsToExecute.length}\n`);
1084
+ process.stderr.write(`[loop-timing] iter=${nextIteration} presend=${preSendMs}ms send=${_sendEndedAt - sendStartedAt}ms tools=${_toolsEndedAt - _toolsT0}ms calls=${_callsToExecute.length}\n`);
1060
1085
  } catch { /* diagnostics only */ }
1061
1086
  }
1087
+ _lastToolBatchEndedAt = _toolsEndedAt;
1062
1088
  _toolBatchJustCompleted = true;
1063
1089
  _continuationsSinceToolBatch = 0;
1064
1090
  _lastToolBatchHadSleep = _callsToExecute.some(isSleepLikeToolCall);
@@ -53,4 +53,4 @@ export {
53
53
  export {
54
54
  semanticCompactMessages,
55
55
  recallFastTrackCompactMessages,
56
- } from './compact/engine.mjs';
56
+ } from './compact/runner.mjs';
@@ -1,13 +1,19 @@
1
1
  // Eager tool-dispatch controller, extracted from agent-loop.mjs. Owns the
2
2
  // per-turn pending promise map, the intra-turn in-flight signature set, and
3
- // the mutation epoch. Read-only calls may start while the provider is still
4
- // streaming. Side-effect calls stream-start too until the first apply_patch
5
- // appears in the stream (they are definitively in segment 0); after that they
6
- // wait for their segment bounded by apply_patch barriers. Patches execute
7
- // serially as barriers, so a failed patch can skip every later side effect.
3
+ // the mutation epoch. Every valid call dispatches while the provider is still
4
+ // streaming. Calls execute in parallel except that shell after apply_patch
5
+ // waits for every earlier patch in the turn; results are collected later in
6
+ // call order.
8
7
  import { normalizeToolEnvelope } from './tool-envelope.mjs';
9
8
  import { isInvalidToolArgsMarker } from '../providers/openai-compat-stream.mjs';
10
- import { _intraTurnSig, _isMutationTool, _isOrderedGateSkippable, _isReadTool, _isScopedCacheableTool, _stripMcpPrefix } from './loop/tool-classify.mjs';
9
+ import {
10
+ _intraTurnSig,
11
+ _isMutationTool,
12
+ _isReadTool,
13
+ _isScopedCacheableTool,
14
+ _isShellTool,
15
+ _stripMcpPrefix,
16
+ } from './loop/tool-classify.mjs';
11
17
  import { tryReadCached, tryScopedToolCached } from './read-dedup.mjs';
12
18
  import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
13
19
  import { executeTool } from './loop/tool-exec.mjs';
@@ -20,6 +26,10 @@ export function createEagerDispatcher({
20
26
  executeToolFn = executeTool,
21
27
  }) {
22
28
  const pending = new Map();
29
+ // Cumulative settlement barrier for patches already emitted in this
30
+ // assistant turn. Patches remain path-parallel with each other; only a
31
+ // later shell waits, so validation cannot observe pre-patch files.
32
+ let patchBarrier = Promise.resolve();
23
33
  // Streaming-time intra-turn dedup. When the LLM emits two
24
34
  // tool_use blocks with identical (name, args) signatures in
25
35
  // sequence, the provider's onToolCall fires for both BEFORE
@@ -40,11 +50,6 @@ export function createEagerDispatcher({
40
50
  // resets at the turn boundary without leaking across getIterations().
41
51
  const _eagerInFlightSigs = new Map();
42
52
  const epoch = { mutation: 0 };
43
- // True once an apply_patch tool_use has been seen in THIS turn's
44
- // stream. Before that, every side-effect call streamed so far sits
45
- // before the first patch in call order (segment 0), so it may start
46
- // immediately — a patch that arrives later cannot gate an EARLIER call.
47
- let _streamSeenMutation = false;
48
53
  const startEagerTool = (call) => {
49
54
  if (!call?.id || pending.has(call.id) || !isParallelDispatchable(call.name)) return null;
50
55
  // Never eager-execute a call whose arguments failed to parse
@@ -97,9 +102,11 @@ export function createEagerDispatcher({
97
102
  // this call there, never start it eagerly here.
98
103
  if (preDispatchDenyForSession(sessionRef, call, toolKind) !== null) return null;
99
104
  const entry = { startedAt: Date.now(), endedAt: null, mutationEpoch: epoch.mutation };
105
+ const precedingPatches = _isShellTool(call.name) ? patchBarrier : null;
100
106
  if (_dedupEligible) _eagerInFlightSigs.set(_sig, call.id);
101
107
  entry.promise = (async () => {
102
108
  try {
109
+ if (precedingPatches) await precedingPatches;
103
110
  await opts.beforeToolExecution?.();
104
111
  return { ok: true, value: await executeToolFn(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: getNextIteration(), deferShellCwdCommit: true }) };
105
112
  } catch (error) {
@@ -159,24 +166,17 @@ export function createEagerDispatcher({
159
166
  return settled;
160
167
  });
161
168
  pending.set(call.id, entry);
169
+ if (_isMutationTool(call.name)) {
170
+ const currentPatch = entry.promise;
171
+ patchBarrier = Promise.allSettled([patchBarrier, currentPatch]).then(() => undefined);
172
+ }
162
173
  return entry;
163
174
  };
164
175
  const startEagerRun = (calls, startIndex, dupSet) => {
165
- const _nextOrderedMutationIndex = calls.findIndex(
166
- (call, index) => index >= startIndex && _isMutationTool(call?.name),
167
- );
168
176
  for (let j = startIndex; j < calls.length; j += 1) {
169
177
  const call = calls[j];
170
- // Side effects may eager-start in the current segment but not
171
- // across the next apply_patch barrier. Known read-only work
172
- // may cross it and is protected by mutation-epoch re-execution.
173
178
  if (!call?.id || !isParallelDispatchable(call.name)) continue;
174
179
  if (dupSet && dupSet.has(call.id)) continue;
175
- if (
176
- _nextOrderedMutationIndex >= 0
177
- && j >= _nextOrderedMutationIndex
178
- && _isOrderedGateSkippable(call.name)
179
- ) continue;
180
180
  // A null return here is NOT a state barrier. It means a
181
181
  // non-barrier stub — intra-turn in-flight dup, repeat-failure /
182
182
  // cross-turn dedup, pre-dispatch-deny, invalid-args, or a cache
@@ -187,17 +187,6 @@ export function createEagerDispatcher({
187
187
  }
188
188
  };
189
189
  const onToolCall = (call) => {
190
- if (_isMutationTool(call?.name)) {
191
- _streamSeenMutation = true;
192
- return;
193
- }
194
- // Declared read-only calls always overlap streaming (epoch guard
195
- // re-executes them if a later patch lands). Side-effect calls may
196
- // stream-start only while NO apply_patch has streamed yet: their
197
- // call-order position is already fixed before the first barrier,
198
- // matching exactly what startEagerRun would do post-batch. Once a
199
- // patch has streamed, later side effects wait for their segment.
200
- if (!isEagerDispatchable(call?.name, tools) && _streamSeenMutation) return;
201
190
  startEagerTool(call);
202
191
  };
203
192
  return { pending, epoch, startEagerTool, startEagerRun, onToolCall };
@@ -71,6 +71,19 @@ export function isLoopbackHttpUrl(value) {
71
71
  */
72
72
  export function routeWebFetchCall(call) {
73
73
  if (call?.name !== 'web_fetch') return call;
74
+ // A JSON-stringified array ('["https://a","https://b"]') is a common
75
+ // provider serialisation artifact. Parse it back into real URLs so the
76
+ // fetch surface routes and validates them instead of rejecting the call.
77
+ const rawUrl = call?.arguments?.url;
78
+ if (typeof rawUrl === 'string' && /^\s*\[/.test(rawUrl)) {
79
+ try {
80
+ const parsed = JSON.parse(rawUrl);
81
+ if (Array.isArray(parsed) && parsed.length > 0
82
+ && parsed.every((item) => typeof item === 'string' && item.trim())) {
83
+ call.arguments.url = parsed.map((item) => item.trim());
84
+ }
85
+ } catch { /* leave the original value for the downstream validator */ }
86
+ }
74
87
  const urls = callUrls(call);
75
88
  if (!urls.length) return call;
76
89
  if (urls.every(isLoopbackHttpUrl)) call.name = 'local_fetch';
@@ -28,8 +28,8 @@ export function isOutputLimitStopReason(reason) {
28
28
  // NOT a completed assistant turn even when it holds final-looking text: the
29
29
  // provider explicitly stated it intends to keep sampling inside the SAME user
30
30
  // turn. Two structured shapes are honored:
31
- // - `endTurn === false` (Codex/Responses `end_turn=false`; see
32
- // codex-rs/core/src/session/turn.rs Completed{ end_turn } → needs_follow_up)
31
+ // - `endTurn === false` (Responses `end_turn=false` on the completed frame,
32
+ // which means the turn needs a follow-up)
33
33
  // - Anthropic `stop_reason: 'pause_turn'` (server-side pause; the turn is
34
34
  // resumed by replaying the transcript unchanged)
35
35
  // Structural only — no lexical/progress-text heuristic is consulted, and a
@@ -15,30 +15,6 @@ export function _isMutationTool(name) {
15
15
  const n = _stripMcpPrefix(name);
16
16
  return n === 'apply_patch';
17
17
  }
18
- // Side-effect-free read-only tools that may eager-start across an upcoming
19
- // apply_patch barrier and may keep running after an earlier patch fails.
20
- // Everything NOT in this set waits for its side-effect segment and is skipped
21
- // after a failed patch. Kept separate from _isMutationTool, which stays
22
- // apply_patch-only for epoch counting.
23
- const ORDERED_GATE_SAFE_READONLY_TOOLS = new Set([
24
- 'read',
25
- 'find',
26
- 'glob',
27
- 'list',
28
- 'grep',
29
- 'code_graph',
30
- 'explore',
31
- 'search',
32
- 'web_fetch',
33
- ]);
34
- // True when a LATER call in a batch must be skipped because an earlier ordered
35
- // mutation (apply_patch) already failed. Conservative: only the known
36
- // side-effect-free read-only tools above keep running; shell, write tools, and
37
- // unknown MCP tools are skipped so they cannot act on unchanged/partially
38
- // changed state.
39
- export function _isOrderedGateSkippable(name) {
40
- return !ORDERED_GATE_SAFE_READONLY_TOOLS.has(_stripMcpPrefix(name));
41
- }
42
18
  const SCOPED_CACHEABLE_TOOLS = new Set([
43
19
  'code_graph',
44
20
  'grep',