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
@@ -4,19 +4,29 @@ import { requestMemoryPressureSnapshot } from './memory-snapshot.mjs';
4
4
 
5
5
  const MB = 1024 * 1024;
6
6
 
7
- // Concurrency defaults are UNBOUNDED by design: parallel agent/shell waves
8
- // must never serialize behind an admission queue. The only default guards are
9
- // the memory-pressure checks below; hard caps exist solely as opt-in env
10
- // overrides (MIXDOG_MAX_CONCURRENT_AGENTS / _SHELLS / _HIGH_LOAD).
7
+ // The unified daemon queues bursts instead of starting an unbounded number of
8
+ // agent/provider/shell tasks in one process. Environment overrides remain
9
+ // available for larger hosts; memory thresholds are diagnostic only.
11
10
  export const RESOURCE_ADMISSION_DEFAULTS = Object.freeze({
12
- maxAgents: Infinity,
13
- maxShells: Infinity,
14
- maxHighLoad: Infinity,
15
- maxQueue: 32,
16
- minFreeMemoryMb: 1024,
17
- maxRssMb: 3072,
11
+ maxAgents: 8,
12
+ maxShells: 8,
13
+ maxHighLoad: 12,
14
+ maxQueue: 1024,
15
+ minFreeMemoryMb: 0,
16
+ maxRssMb: 0,
18
17
  });
19
18
 
19
+ const PRIORITY_RANK = Object.freeze({
20
+ 'user-blocking': 0,
21
+ 'user-visible': 1,
22
+ 'best-effort': 2,
23
+ });
24
+
25
+ function normalizePriority(value) {
26
+ const key = String(value || 'user-visible').toLowerCase();
27
+ return Object.hasOwn(PRIORITY_RANK, key) ? key : 'user-visible';
28
+ }
29
+
20
30
  function positiveInt(value, fallback) {
21
31
  const parsed = Math.floor(Number(value));
22
32
  return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
@@ -54,19 +64,6 @@ function abortError(signal) {
54
64
  : new Error(String(signal?.reason || 'resource admission canceled'));
55
65
  }
56
66
 
57
- function sleep(ms, signal) {
58
- return new Promise((resolve) => {
59
- const finish = () => {
60
- clearTimeout(timer);
61
- signal?.removeEventListener?.('abort', finish);
62
- resolve();
63
- };
64
- const timer = setTimeout(finish, ms);
65
- timer.unref?.();
66
- signal?.addEventListener?.('abort', finish, { once: true });
67
- });
68
- }
69
-
70
67
  class ResourcePressureError extends Error {
71
68
  constructor(message, details = {}) {
72
69
  super(`resource pressure: ${message}`);
@@ -102,48 +99,37 @@ export class ResourceAdmissionController {
102
99
  this.metrics = metrics;
103
100
  this.now = now;
104
101
  this.active = { agent: 0, shell: 0 };
102
+ this.activeByOwner = { agent: new Map(), shell: new Map() };
103
+ this.fairCursor = { agent: null, shell: null };
105
104
  this.queue = [];
106
105
  // Live leases for saturation diagnostics only (labels/ages in snapshot()).
107
106
  this.activeLeases = new Set();
108
107
  this.context = new AsyncLocalStorage();
109
- // Host free memory is an INSTANTANEOUS sample: a GC pause or a just-exited
110
- // child can dip it below the floor for a few hundred ms. Re-measure a
111
- // bounded number of times before refusing, so a transient dip costs
112
- // latency instead of a failed tool call.
113
- this.memoryRetry = {
114
- attempts: nonNegativeInt(env.MIXDOG_MEMORY_PRESSURE_RETRIES, 3),
115
- delayMs: nonNegativeInt(env.MIXDOG_MEMORY_PRESSURE_RETRY_MS, 400),
116
- };
117
108
  }
118
109
 
119
- _memoryError(kind) {
110
+ _recordMemoryPressure(kind) {
111
+ // Thresholds are an operator-opt-in telemetry trigger only. The OS owns
112
+ // memory pressure policy; Mixdog never turns a valid agent/shell request
113
+ // into a failed task because of an RSS or host-free-memory sample.
114
+ if (this.limits.maxRssMb <= 0 && this.limits.minFreeMemoryMb <= 0) return false;
120
115
  let sample;
121
116
  try { sample = this.metrics() || {}; }
122
- catch (error) {
123
- return new ResourcePressureError(`memory metrics unavailable; refusing new ${kind} work`, {
124
- kind,
125
- cause: error,
126
- });
127
- }
117
+ catch { return false; }
128
118
  const rssMb = Number(sample.rssBytes) / MB;
129
119
  const freeMb = Number(sample.freeMemoryBytes) / MB;
130
120
  if (this.limits.maxRssMb > 0 && Number.isFinite(rssMb) && rssMb >= this.limits.maxRssMb) {
131
- const error = new ResourcePressureError(
132
- `Mixdog RSS ${Math.ceil(rssMb)} MB reached ${this.limits.maxRssMb} MB limit; retry after memory recovers`,
133
- { kind, rssMb, limitMb: this.limits.maxRssMb, metric: 'rss' },
121
+ requestMemoryPressureSnapshot(
122
+ `diagnostic threshold: Mixdog RSS ${Math.ceil(rssMb)} MB reached ${this.limits.maxRssMb} MB while starting ${kind}`,
134
123
  );
135
- requestMemoryPressureSnapshot(error.message);
136
- return error;
124
+ return true;
137
125
  }
138
126
  if (this.limits.minFreeMemoryMb > 0 && Number.isFinite(freeMb) && freeMb < this.limits.minFreeMemoryMb) {
139
- const error = new ResourcePressureError(
140
- `host free memory ${Math.floor(freeMb)} MB is below ${this.limits.minFreeMemoryMb} MB minimum; retry after memory recovers`,
141
- { kind, freeMb, limitMb: this.limits.minFreeMemoryMb, metric: 'free-memory' },
127
+ requestMemoryPressureSnapshot(
128
+ `diagnostic threshold: host free memory ${Math.floor(freeMb)} MB is below ${this.limits.minFreeMemoryMb} MB while starting ${kind}`,
142
129
  );
143
- requestMemoryPressureSnapshot(error.message);
144
- return error;
130
+ return true;
145
131
  }
146
- return null;
132
+ return false;
147
133
  }
148
134
 
149
135
  _canStart(kind) {
@@ -158,6 +144,12 @@ export class ResourceAdmissionController {
158
144
  if (parent.counted) {
159
145
  parent.counted = false;
160
146
  this.active[parent.kind] = Math.max(0, this.active[parent.kind] - 1);
147
+ if (parent.ownerKey) {
148
+ const owners = this.activeByOwner[parent.kind];
149
+ const count = Math.max(0, (owners.get(parent.ownerKey) || 0) - 1);
150
+ if (count > 0) owners.set(parent.ownerKey, count);
151
+ else owners.delete(parent.ownerKey);
152
+ }
161
153
  }
162
154
  return parent;
163
155
  }
@@ -175,6 +167,8 @@ export class ResourceAdmissionController {
175
167
  queuedAt: this.now(),
176
168
  signal: parent.signal,
177
169
  parent,
170
+ ownerKey: parent.ownerKey,
171
+ priority: parent.priority,
178
172
  resolve: pending.resolve,
179
173
  reject: pending.reject,
180
174
  canceled: false,
@@ -197,8 +191,10 @@ export class ResourceAdmissionController {
197
191
  return pending.promise;
198
192
  }
199
193
 
200
- _lease(kind, queuedAt = null, parent = null) {
194
+ _lease(kind, queuedAt = null, parent = null, ownerKey = '', priority = 'user-visible') {
201
195
  this.active[kind] += 1;
196
+ const owner = String(ownerKey || '');
197
+ if (owner) this.activeByOwner[kind].set(owner, (this.activeByOwner[kind].get(owner) || 0) + 1);
202
198
  const lease = {
203
199
  controller: this,
204
200
  kind,
@@ -209,6 +205,8 @@ export class ResourceAdmissionController {
209
205
  dependencyDepth: 0,
210
206
  restorePending: null,
211
207
  parent,
208
+ ownerKey: owner,
209
+ priority: normalizePriority(priority),
212
210
  releasePromise: null,
213
211
  startedAt: this.now(),
214
212
  queuedMs: queuedAt == null ? 0 : Math.max(0, this.now() - queuedAt),
@@ -227,6 +225,11 @@ export class ResourceAdmissionController {
227
225
  if (lease.counted) {
228
226
  lease.counted = false;
229
227
  this.active[kind] = Math.max(0, this.active[kind] - 1);
228
+ if (lease.ownerKey) {
229
+ const count = Math.max(0, (this.activeByOwner[kind].get(lease.ownerKey) || 0) - 1);
230
+ if (count > 0) this.activeByOwner[kind].set(lease.ownerKey, count);
231
+ else this.activeByOwner[kind].delete(lease.ownerKey);
232
+ }
230
233
  }
231
234
  lease.releasePromise = this._resumeParent(lease.parent);
232
235
  this._drain();
@@ -252,29 +255,15 @@ export class ResourceAdmissionController {
252
255
  return this.context.run(lease, task);
253
256
  }
254
257
 
255
- async _acquireAfterMemoryDip(kind, options, pressure) {
256
- const lane = kind === 'shell' ? 'shell' : 'agent';
257
- const { attempts, delayMs } = this.memoryRetry;
258
- for (let attempt = 0; attempt < attempts; attempt += 1) {
259
- await sleep(delayMs, options.signal);
260
- if (options.signal?.aborted) throw abortError(options.signal);
261
- if (!this._memoryError(lane)) return this.acquire(kind, options);
262
- }
263
- throw this._memoryError(lane) || pressure;
264
- }
265
-
266
- acquire(kind, { signal = null, label = null, dependency = 'scoped' } = {}) {
258
+ acquire(kind, {
259
+ signal = null, label = null, dependency = 'scoped', ownerKey = null,
260
+ priority = 'user-visible',
261
+ } = {}) {
267
262
  const lane = kind === 'shell' ? 'shell' : 'agent';
263
+ const owner = ownerKey == null ? '' : String(ownerKey).trim().slice(0, 240);
264
+ const taskPriority = normalizePriority(priority);
268
265
  if (signal?.aborted) return Promise.reject(abortError(signal));
269
- const pressure = this._memoryError(lane);
270
- if (pressure) {
271
- // Only host free memory self-heals on this timescale; our own RSS does
272
- // not, so RSS pressure still rejects immediately.
273
- if (pressure.metric !== 'free-memory' || this.memoryRetry.attempts < 1) {
274
- return Promise.reject(pressure);
275
- }
276
- return this._acquireAfterMemoryDip(kind, { signal, label, dependency }, pressure);
277
- }
266
+ this._recordMemoryPressure(lane);
278
267
  const ambientParent = this.context.getStore();
279
268
  const detachedDependency = dependency === 'detached' && ambientParent?.controller === this;
280
269
  const parent = detachedDependency ? null : this._suspendParent(ambientParent);
@@ -287,7 +276,7 @@ export class ResourceAdmissionController {
287
276
  return Promise.reject(error);
288
277
  }
289
278
  if (this._canStart(lane) && this.queue.length === 0) {
290
- const lease = this._lease(lane, null, parent);
279
+ const lease = this._lease(lane, null, parent, owner, taskPriority);
291
280
  lease.label = label;
292
281
  lease.signal = signal;
293
282
  return Promise.resolve(lease);
@@ -310,6 +299,8 @@ export class ResourceAdmissionController {
310
299
  canceled: false,
311
300
  onAbort: null,
312
301
  parent,
302
+ ownerKey: owner,
303
+ priority: taskPriority,
313
304
  };
314
305
  if (signal) {
315
306
  item.onAbort = () => {
@@ -339,35 +330,64 @@ export class ResourceAdmissionController {
339
330
  }
340
331
  }
341
332
 
342
- _drain() {
343
- for (let index = 0; index < this.queue.length;) {
333
+ _nextFairIndex() {
334
+ const runnable = [];
335
+ for (let index = 0; index < this.queue.length; index += 1) {
344
336
  const item = this.queue[index];
337
+ if (item.canceled || item.signal?.aborted) continue;
338
+ if (item.restore && item.parent.released) continue;
339
+ if (!this._canStart(item.kind)) continue;
340
+ runnable.push({ index, item });
341
+ }
342
+ if (runnable.length === 0) return -1;
343
+ // USER_BLOCKING work (abort/recovery), then USER_VISIBLE tool work, then
344
+ // BEST_EFFORT maintenance. Preserve FIFO between resource kinds inside a
345
+ // priority and round-robin sessions within that lane.
346
+ const bestRank = Math.min(...runnable.map(({ item }) =>
347
+ PRIORITY_RANK[normalizePriority(item.priority)]));
348
+ const prioritized = runnable.filter(({ item }) =>
349
+ PRIORITY_RANK[normalizePriority(item.priority)] === bestRank);
350
+ const lane = prioritized[0].item.kind;
351
+ const laneRows = prioritized.filter((row) => row.item.kind === lane);
352
+ const owners = [];
353
+ for (const { item } of laneRows) {
354
+ const owner = item.ownerKey || '';
355
+ if (!owners.includes(owner)) owners.push(owner);
356
+ }
357
+ let selected = owners[0] || '';
358
+ if (owners.length > 1) {
359
+ const previous = this.fairCursor[lane];
360
+ const previousIndex = owners.indexOf(previous);
361
+ selected = owners[(previousIndex + 1 + owners.length) % owners.length];
362
+ }
363
+ this.fairCursor[lane] = selected;
364
+ return laneRows.find((row) => (row.item.ownerKey || '') === selected)?.index ?? laneRows[0].index;
365
+ }
366
+
367
+ _drain() {
368
+ for (;;) {
369
+ // Remove dead/cancelled rows before selecting an owner. Their callbacks
370
+ // may enqueue restoration work, so restart selection after each cleanup.
371
+ let cleaned = false;
372
+ for (let index = 0; index < this.queue.length; index += 1) {
373
+ const item = this.queue[index];
345
374
  if (item.restore) {
346
375
  if (item.parent.released) {
347
376
  this.queue.splice(index, 1);
348
377
  this._detach(item);
349
378
  item.parent.restorePending = null;
350
379
  item.resolve();
351
- continue;
380
+ cleaned = true;
381
+ break;
352
382
  }
353
383
  if (item.signal?.aborted) {
354
384
  this.queue.splice(index, 1);
355
385
  this._detach(item);
356
386
  item.parent.restorePending = null;
357
387
  item.reject(abortError(item.signal));
358
- continue;
388
+ cleaned = true;
389
+ break;
359
390
  }
360
- if (!this._canStart(item.kind)) {
361
- index += 1;
362
- continue;
363
- }
364
- this.queue.splice(index, 1);
365
- this._detach(item);
366
- item.parent.restorePending = null;
367
- item.parent.counted = true;
368
- this.active[item.kind] += 1;
369
- item.resolve();
370
- continue;
371
391
  }
372
392
  if (item.canceled || item.signal?.aborted) {
373
393
  this.queue.splice(index, 1);
@@ -377,23 +397,37 @@ export class ResourceAdmissionController {
377
397
  () => item.reject(error),
378
398
  (restoreError) => item.reject(restoreError),
379
399
  );
380
- continue;
381
- }
382
- if (!this._canStart(item.kind)) {
383
- index += 1;
384
- continue;
400
+ cleaned = true;
401
+ break;
402
+ }
385
403
  }
386
- const pressure = this._memoryError(item.kind);
404
+ if (cleaned) continue;
405
+ const index = this._nextFairIndex();
406
+ if (index < 0) return;
407
+ const item = this.queue[index];
408
+ this._recordMemoryPressure(item.kind);
387
409
  this.queue.splice(index, 1);
388
410
  this._detach(item);
389
- if (pressure) {
390
- this._resumeParent(item.parent).then(
391
- () => item.reject(pressure),
392
- (restoreError) => item.reject(restoreError),
393
- );
411
+ if (item.restore) {
412
+ item.parent.restorePending = null;
413
+ item.parent.counted = true;
414
+ this.active[item.kind] += 1;
415
+ if (item.ownerKey) {
416
+ this.activeByOwner[item.kind].set(
417
+ item.ownerKey,
418
+ (this.activeByOwner[item.kind].get(item.ownerKey) || 0) + 1,
419
+ );
420
+ }
421
+ item.resolve();
394
422
  continue;
395
423
  }
396
- const lease = this._lease(item.kind, item.queuedAt, item.parent);
424
+ const lease = this._lease(
425
+ item.kind,
426
+ item.queuedAt,
427
+ item.parent,
428
+ item.ownerKey,
429
+ item.priority,
430
+ );
397
431
  lease.label = item.label;
398
432
  lease.signal = item.signal;
399
433
  item.resolve(lease);
@@ -402,15 +436,31 @@ export class ResourceAdmissionController {
402
436
 
403
437
  snapshot() {
404
438
  const now = this.now();
439
+ const wireLimit = (value) => Number.isFinite(value) ? value : null;
405
440
  return {
406
441
  active: { ...this.active },
442
+ activeOwners: {
443
+ agent: this.activeByOwner.agent.size,
444
+ shell: this.activeByOwner.shell.size,
445
+ },
407
446
  queued: this.queue.length,
408
- limits: { ...this.limits },
447
+ limits: {
448
+ ...this.limits,
449
+ maxAgents: wireLimit(this.limits.maxAgents),
450
+ maxShells: wireLimit(this.limits.maxShells),
451
+ maxHighLoad: wireLimit(this.limits.maxHighLoad),
452
+ },
409
453
  activeLeases: [...this.activeLeases].map((lease) => ({
410
454
  kind: lease.kind,
411
455
  label: lease.label,
456
+ ownerKey: lease.ownerKey || null,
457
+ priority: lease.priority,
412
458
  ageMs: Math.max(0, now - (lease.startedAt || now)),
413
459
  })),
460
+ oldestQueuedMs: this.queue.reduce(
461
+ (oldest, item) => Math.max(oldest, Math.max(0, now - (item.queuedAt || now))),
462
+ 0,
463
+ ),
414
464
  };
415
465
  }
416
466
  }
@@ -0,0 +1,6 @@
1
+ // Provider adapters also report transport/ack progress so stall watchdogs can
2
+ // distinguish a live connection from a dead one. Those events are not visible
3
+ // model output and must not satisfy user-facing TTFT or launch heavy warmups.
4
+ export function isVisibleStreamProgress(kind) {
5
+ return kind === 'text' || kind === 'reasoning' || kind === 'tool';
6
+ }
@@ -0,0 +1,12 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
3
+ const toolExecutionOwner = new AsyncLocalStorage();
4
+
5
+ export function runWithToolExecutionOwner(ownerKey, run) {
6
+ const owner = String(ownerKey || '').trim().slice(0, 240) || 'anonymous';
7
+ return toolExecutionOwner.run(owner, run);
8
+ }
9
+
10
+ export function currentToolExecutionOwner() {
11
+ return toolExecutionOwner.getStore() || 'anonymous';
12
+ }
@@ -24,7 +24,7 @@ function countNonEmptyLines(text) {
24
24
 
25
25
  // Zero-result recognizer (audit HIGH): result text that SAYS "nothing found"
26
26
  // must summarize as an explicit zero, not be line-counted into "1 match".
27
- // Matches the shapes emitted by grep/glob/find/list/recall backends:
27
+ // Matches the shapes emitted by grep/glob/find/list/recall providers:
28
28
  // "(no matches)", "no matches found", "(no results)", "No results",
29
29
  // "(no fuzzy match for \"...\")", "0 matches", "(empty)", "(no entries)".
30
30
  function looksLikeZeroResultText(text) {
@@ -70,6 +70,40 @@ export {
70
70
  isMemorySurface,
71
71
  };
72
72
 
73
+ function patchOperationProfile(args = {}) {
74
+ const a = parseToolArgs(args);
75
+ const patchText = String(a.patch ?? '');
76
+ const counts = new Map();
77
+ const add = (kind, count = 1) => {
78
+ counts.set(kind, Number(counts.get(kind) || 0) + Math.max(1, Number(count || 1)));
79
+ };
80
+
81
+ for (const line of patchText.split('\n')) {
82
+ const match = /^\*\*\*\s+(Update|Add|Delete) File:\s+.+\s*$/i.exec(line);
83
+ if (!match) continue;
84
+ add(match[1].toLowerCase());
85
+ }
86
+ if (counts.size > 0) return counts;
87
+
88
+ const gitSections = patchText
89
+ .split(/(?=^diff --git )/m)
90
+ .filter((section) => /^diff --git /m.test(section));
91
+ const unifiedSections = gitSections.length > 0
92
+ ? gitSections
93
+ : (/^---\s+.+\n\+\+\+\s+.+$/m.test(patchText) ? [patchText] : []);
94
+ for (const section of unifiedSections) {
95
+ if (/^new file mode /m.test(section) || /^---\s+\/dev\/null(?:\s|$)/m.test(section)) add('add');
96
+ else if (/^deleted file mode /m.test(section) || /^\+\+\+\s+\/dev\/null(?:\s|$)/m.test(section)) add('delete');
97
+ else add('update');
98
+ }
99
+ if (counts.size > 0) return counts;
100
+
101
+ if (a.old_string === '') add('add');
102
+ else if (a.new_string === '' && a.old_string != null) add('delete');
103
+ else add('update', patchFileCount(a) || 1);
104
+ return counts;
105
+ }
106
+
73
107
  export function displayToolName(name, args = {}) {
74
108
  if (isExternalMcpToolName(name)) {
75
109
  const mcp = parseMcpToolName(name);
@@ -84,7 +118,11 @@ export function displayToolName(name, args = {}) {
84
118
  case 'apply_patch': {
85
119
  const parsed = parseToolArgs(args);
86
120
  if (parsed && parsed.dry_run === true) return 'Check';
87
- return parsed && parsed.old_string === '' ? 'Create' : 'Update';
121
+ const operations = patchOperationProfile(parsed);
122
+ if (operations.size > 1) return 'Change';
123
+ if (operations.has('add')) return 'Create';
124
+ if (operations.has('delete')) return 'Delete';
125
+ return 'Update';
88
126
  }
89
127
  case 'shell':
90
128
  case 'bash':
@@ -433,6 +471,29 @@ function queryCount(args, ...keys) {
433
471
  return collectionCount(...keys.map((key) => args?.[key]));
434
472
  }
435
473
 
474
+ function patchMutationUnits(args = {}) {
475
+ const a = parseToolArgs(args);
476
+ if (a.dry_run === true) {
477
+ return [unitDescriptor('Patch', {
478
+ count: patchFileCount(a) || 1,
479
+ active: 'Checking',
480
+ done: 'Checked',
481
+ noun: 'file',
482
+ })];
483
+ }
484
+ const copy = {
485
+ add: { active: 'Creating', done: 'Created' },
486
+ delete: { active: 'Deleting', done: 'Deleted' },
487
+ update: { active: 'Editing', done: 'Edited' },
488
+ };
489
+ return [...patchOperationProfile(a)].map(([kind, count]) => unitDescriptor('Patch', {
490
+ count,
491
+ active: copy[kind]?.active || 'Editing',
492
+ done: copy[kind]?.done || 'Edited',
493
+ noun: 'file',
494
+ }));
495
+ }
496
+
436
497
  export function toolWorkUnit(name, args = {}, category = '') {
437
498
  const a = parseToolArgs(args);
438
499
  const normalized = normalizeToolName(name);
@@ -452,30 +513,12 @@ export function toolWorkUnit(name, args = {}, category = '') {
452
513
  case 'read_mcp_resource':
453
514
  return unitDescriptor('Read', { count: queryCount(a, 'uri', 'uris') || 1, noun: 'resource' });
454
515
  case 'apply_patch': {
455
- const patchText = String(a.patch ?? '');
456
- const creating = a.old_string === '' || /^\*\*\*\s+Add File:/mi.test(patchText);
457
- const deleting = (!creating && a.new_string === '' && a.old_string != null)
458
- || /^\*\*\*\s+Delete File:/mi.test(patchText);
459
- // A dry_run patch validates the diff WITHOUT writing any file, so the
460
- // header must not claim "Editing/Edited" (which made a pure validation
461
- // look like a real edit). Surface it as "Checking/Checked" instead.
462
- if (a.dry_run === true) {
463
- return unitDescriptor('Patch', {
464
- count: patchFileCount(a) || 1,
465
- active: 'Checking',
466
- done: 'Checked',
467
- noun: 'file',
468
- });
469
- }
516
+ const units = patchMutationUnits(a);
517
+ if (units.length === 1) return units[0];
470
518
  return unitDescriptor('Patch', {
471
- count: patchFileCount(a) || 1,
472
- // post_shell is NOT named here: the turn engine already splits such a
473
- // call into this edit card plus a trailing Shell card that carries the
474
- // real command, its output and its failure banner (turn.mjs
475
- // postShellTwins). Repeating "& verifying" in the header declared the
476
- // verify phase twice and truncated the label on narrow panes.
477
- active: creating ? 'Creating' : deleting ? 'Deleting' : 'Editing',
478
- done: creating ? 'Created' : deleting ? 'Deleted' : 'Edited',
519
+ count: units.reduce((total, unit) => total + unit.count, 0),
520
+ active: 'Changing',
521
+ done: 'Changed',
479
522
  noun: 'file',
480
523
  });
481
524
  }
@@ -606,6 +649,26 @@ export function aggregateToolCategoryEntry(name, args = {}, category = '') {
606
649
  };
607
650
  }
608
651
 
652
+ export function aggregateToolCategoryEntries(name, args = {}, category = '') {
653
+ const cat = category || classifyToolCategory(name, args);
654
+ const normalized = normalizeToolName(name);
655
+ const units = normalized === 'apply_patch'
656
+ ? patchMutationUnits(args)
657
+ : [toolWorkUnit(name, args, cat)];
658
+ return units.map((unit) => {
659
+ const key = [cat, unit.active, unit.done, unit.noun, unit.pluralNoun].join('|');
660
+ return {
661
+ key,
662
+ category: cat,
663
+ active: unit.active,
664
+ done: unit.done,
665
+ noun: unit.noun,
666
+ pluralNoun: unit.pluralNoun,
667
+ count: Math.max(1, Number(unit.count || 1)),
668
+ };
669
+ });
670
+ }
671
+
609
672
  /**
610
673
  * Rebuild the per-category count map for the DONE state. Counts ATTEMPTS —
611
674
  * failed calls included — so the collapsed header total always agrees with
@@ -618,9 +681,10 @@ export function aggregateDoneCategories(calls = []) {
618
681
  const map = new Map();
619
682
  for (const rec of calls || []) {
620
683
  if (!rec) continue;
621
- const entry = aggregateToolCategoryEntry(rec.name, rec.args, rec.category);
622
- const prev = map.get(entry.key);
623
- map.set(entry.key, { ...entry, count: Number(prev?.count || 0) + Number(entry.count || 1) });
684
+ for (const entry of aggregateToolCategoryEntries(rec.name, rec.args, rec.category)) {
685
+ const prev = map.get(entry.key);
686
+ map.set(entry.key, { ...entry, count: Number(prev?.count || 0) + Number(entry.count || 1) });
687
+ }
624
688
  }
625
689
  return Object.fromEntries(map);
626
690
  }
@@ -0,0 +1,29 @@
1
+ import { createOwnerFairGate } from './owner-fair-gate.mjs';
2
+
3
+ function positiveInt(value, fallback) {
4
+ const parsed = Math.floor(Number(value));
5
+ return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
6
+ }
7
+
8
+ export const readIoAdmission = createOwnerFairGate({
9
+ name: 'read I/O',
10
+ activeMax: positiveInt(process.env.MIXDOG_READ_MAX_INFLIGHT, 16),
11
+ queueMax: positiveInt(process.env.MIXDOG_READ_MAX_QUEUE, 2048),
12
+ minOwnerQueue: 16,
13
+ waitTimeoutMs: positiveInt(process.env.MIXDOG_READ_WAIT_TIMEOUT_MS, 30_000),
14
+ });
15
+
16
+ export const codeGraphSourceIoAdmission = createOwnerFairGate({
17
+ name: 'code graph source I/O',
18
+ activeMax: positiveInt(process.env.MIXDOG_CODE_GRAPH_SOURCE_MAX_INFLIGHT, 16),
19
+ queueMax: positiveInt(process.env.MIXDOG_CODE_GRAPH_SOURCE_MAX_QUEUE, 2048),
20
+ minOwnerQueue: 16,
21
+ waitTimeoutMs: positiveInt(process.env.MIXDOG_CODE_GRAPH_SOURCE_WAIT_TIMEOUT_MS, 30_000),
22
+ });
23
+
24
+ export function toolWorkloadSnapshot() {
25
+ return {
26
+ readIo: readIoAdmission.snapshot(),
27
+ codeGraphSourceIo: codeGraphSourceIoAdmission.snapshot(),
28
+ };
29
+ }