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,23 @@
1
+ export function createKeyedSingleflight() {
2
+ const pending = new Map();
3
+ return {
4
+ run(key, task) {
5
+ const lane = String(key || '');
6
+ const existing = pending.get(lane);
7
+ if (existing) return existing;
8
+ let tracked;
9
+ const promise = Promise.resolve().then(task);
10
+ tracked = promise.finally(() => {
11
+ if (pending.get(lane) === tracked) pending.delete(lane);
12
+ });
13
+ pending.set(lane, tracked);
14
+ return tracked;
15
+ },
16
+ has(key) {
17
+ return pending.has(String(key || ''));
18
+ },
19
+ get size() {
20
+ return pending.size;
21
+ },
22
+ };
23
+ }
@@ -1,10 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
3
 
4
- // Unbounded by design: provider requests must never serialize behind a local
5
- // concurrency cap. Rate-limit cooldowns below (server-mandated retry-after)
6
- // remain the only thing that parks/rejects requests; with an infinite limit
7
- // the adaptive halving/recovery below is inert.
4
+ // Normal provider traffic is not concurrency-gated. Independent sessions and
5
+ // accounts start immediately; a finite limit exists only when an operator
6
+ // explicitly configures one, or after that account reports a real 429.
8
7
  export const PROVIDER_ACCOUNT_CONCURRENCY = Infinity;
9
8
  export const PROVIDER_ACCOUNT_MAX_QUEUE = 1024;
10
9
  // A cooldown longer than this is a quota-window block (subscription limit),
@@ -13,6 +12,21 @@ export const PROVIDER_ACCOUNT_MAX_QUEUE = 1024;
13
12
  export const PROVIDER_COOLDOWN_FAIL_FAST_MS = 60_000;
14
13
  const MAX_TIMEOUT_MS = 2_147_483_647;
15
14
  const currentAdmission = new AsyncLocalStorage();
15
+ const PRIORITY_RANK = Object.freeze({
16
+ 'user-blocking': 0,
17
+ 'user-visible': 1,
18
+ 'best-effort': 2,
19
+ });
20
+
21
+ function normalizePriority(value) {
22
+ const key = String(value || 'user-visible').toLowerCase();
23
+ return Object.hasOwn(PRIORITY_RANK, key) ? key : 'user-visible';
24
+ }
25
+
26
+ export function currentProviderAdmissionOwner() {
27
+ const active = currentAdmission.getStore();
28
+ return active?.ownerKey ? String(active.ownerKey) : null;
29
+ }
16
30
 
17
31
  function abortError(signal, fallback = 'provider request canceled') {
18
32
  return signal?.reason instanceof Error
@@ -110,10 +124,10 @@ export class ProviderAdmissionScheduler {
110
124
  setTimer = setTimeout,
111
125
  clearTimer = clearTimeout,
112
126
  } = {}) {
113
- this.concurrency = Math.min(
114
- PROVIDER_ACCOUNT_CONCURRENCY,
115
- Math.max(1, Math.floor(Number(concurrency) || PROVIDER_ACCOUNT_CONCURRENCY)),
116
- );
127
+ const parsedConcurrency = Math.floor(Number(concurrency));
128
+ this.concurrency = Number.isFinite(parsedConcurrency) && parsedConcurrency >= 1
129
+ ? parsedConcurrency
130
+ : Infinity;
117
131
  this.maxQueue = configuredQueueBound(maxQueue);
118
132
  this.now = now;
119
133
  this.setTimer = setTimer;
@@ -160,7 +174,9 @@ export class ProviderAdmissionScheduler {
160
174
  limit: this.concurrency,
161
175
  cooldownUntil: 0,
162
176
  recoverySuccesses: 0,
177
+ recoveryTarget: null,
163
178
  cooldownTimer: null,
179
+ fairCursor: null,
164
180
  };
165
181
  if (!lane.adaptive || until <= lane.cooldownUntil) return false;
166
182
  this.lanes.set(laneKey, lane);
@@ -175,8 +191,15 @@ export class ProviderAdmissionScheduler {
175
191
  return true;
176
192
  }
177
193
 
178
- run(key, task, { signal = null, onCooldownWait = null } = {}) {
194
+ run(key, task, {
195
+ signal = null,
196
+ onCooldownWait = null,
197
+ ownerKey = null,
198
+ priority = 'user-visible',
199
+ } = {}) {
179
200
  const laneKey = String(key || 'provider:default');
201
+ const owner = String(ownerKey || 'anonymous').trim().slice(0, 240) || 'anonymous';
202
+ const taskPriority = normalizePriority(priority);
180
203
  // Provider-local recovery may recursively call this.send(). It already
181
204
  // owns a slot, so reacquiring the same lane could deadlock a full wave.
182
205
  const current = this.context.getStore();
@@ -193,7 +216,9 @@ export class ProviderAdmissionScheduler {
193
216
  limit: this.concurrency,
194
217
  cooldownUntil: 0,
195
218
  recoverySuccesses: 0,
219
+ recoveryTarget: null,
196
220
  cooldownTimer: null,
221
+ fairCursor: null,
197
222
  };
198
223
  this.lanes.set(laneKey, lane);
199
224
  // Long cooldown = quota window. Fail fast with a visible error; short
@@ -206,7 +231,17 @@ export class ProviderAdmissionScheduler {
206
231
  return Promise.reject(new ProviderAdmissionQueueOverflowError(laneKey, this.maxQueue));
207
232
  }
208
233
  return new Promise((resolve, reject) => {
209
- const item = { task, signal, resolve, reject, canceled: false, onAbort: null };
234
+ const item = {
235
+ task,
236
+ signal,
237
+ resolve,
238
+ reject,
239
+ canceled: false,
240
+ onAbort: null,
241
+ ownerKey: owner,
242
+ priority: taskPriority,
243
+ queuedAt: this.now(),
244
+ };
210
245
  if (signal) {
211
246
  item.onAbort = () => {
212
247
  if (item.started || item.canceled) return;
@@ -236,6 +271,28 @@ export class ProviderAdmissionScheduler {
236
271
  });
237
272
  }
238
273
 
274
+ _nextQueueIndex(lane) {
275
+ if (!lane.queue.length) return -1;
276
+ const bestRank = Math.min(...lane.queue.map((item) =>
277
+ PRIORITY_RANK[normalizePriority(item.priority)]));
278
+ const eligible = lane.queue
279
+ .map((item, index) => ({ item, index }))
280
+ .filter(({ item }) => PRIORITY_RANK[normalizePriority(item.priority)] === bestRank);
281
+ const owners = [];
282
+ for (const { item } of eligible) {
283
+ const owner = item.ownerKey || 'anonymous';
284
+ if (!owners.includes(owner)) owners.push(owner);
285
+ }
286
+ let selected = owners[0] || 'anonymous';
287
+ if (owners.length > 1) {
288
+ const prior = owners.indexOf(lane.fairCursor);
289
+ selected = owners[(prior + 1 + owners.length) % owners.length];
290
+ }
291
+ lane.fairCursor = selected;
292
+ return eligible.find(({ item }) => (item.ownerKey || 'anonymous') === selected)?.index
293
+ ?? eligible[0].index;
294
+ }
295
+
239
296
  _drain(key, lane) {
240
297
  if (lane.adaptive && lane.cooldownUntil > this.now()) {
241
298
  if (lane.cooldownUntil - this.now() > PROVIDER_COOLDOWN_FAIL_FAST_MS) {
@@ -245,7 +302,9 @@ export class ProviderAdmissionScheduler {
245
302
  return;
246
303
  }
247
304
  while (lane.active < lane.limit && lane.queue.length) {
248
- const item = lane.queue.shift();
305
+ const index = this._nextQueueIndex(lane);
306
+ if (index < 0) break;
307
+ const [item] = lane.queue.splice(index, 1);
249
308
  if (item.canceled) {
250
309
  this._detach(item);
251
310
  continue;
@@ -268,7 +327,14 @@ export class ProviderAdmissionScheduler {
268
327
  item.signal.addEventListener('abort', running.parentAbort, { once: true });
269
328
  }
270
329
  this.running.add(running);
271
- const admission = { scheduler: this, key, lane, rateLimited: false };
330
+ const admission = {
331
+ scheduler: this,
332
+ key,
333
+ lane,
334
+ ownerKey: item.ownerKey,
335
+ priority: item.priority,
336
+ rateLimited: false,
337
+ };
272
338
  this.context.run(admission, () => Promise.resolve().then(() => item.task(controller.signal)))
273
339
  .then((value) => {
274
340
  // A retry that eventually succeeds is not evidence that
@@ -292,6 +358,25 @@ export class ProviderAdmissionScheduler {
292
358
  if (this._canDeleteLane(lane)) this._deleteLane(key, lane);
293
359
  }
294
360
 
361
+ snapshot() {
362
+ const now = this.now();
363
+ return {
364
+ concurrency: Number.isFinite(this.concurrency) ? this.concurrency : null,
365
+ lanes: [...this.lanes.entries()].map(([key, lane]) => ({
366
+ key,
367
+ active: lane.active,
368
+ queued: lane.queue.length,
369
+ owners: new Set(lane.queue.map((item) => item.ownerKey || 'anonymous')).size,
370
+ limit: Number.isFinite(lane.limit) ? lane.limit : null,
371
+ cooldownMs: Math.max(0, lane.cooldownUntil - now),
372
+ oldestWaitMs: lane.queue.reduce(
373
+ (oldest, item) => Math.max(oldest, Math.max(0, now - (item.queuedAt || now))),
374
+ 0,
375
+ ),
376
+ })),
377
+ };
378
+ }
379
+
295
380
  _recordFailure(key, lane, error) {
296
381
  if (!lane.adaptive) return false;
297
382
  const status = Number(error?.httpStatus || error?.status || error?.response?.status || 0);
@@ -300,7 +385,14 @@ export class ProviderAdmissionScheduler {
300
385
  if (this.reportedRateLimits.has(error)) return false;
301
386
  this.reportedRateLimits.add(error);
302
387
  }
303
- lane.limit = Math.max(1, Math.floor(lane.limit / 2));
388
+ const observed = Number.isFinite(lane.limit)
389
+ ? lane.limit
390
+ : Math.max(1, lane.active);
391
+ lane.recoveryTarget = Math.max(
392
+ Number(lane.recoveryTarget) || 0,
393
+ observed,
394
+ );
395
+ lane.limit = Math.max(1, Math.floor(observed / 2));
304
396
  lane.recoverySuccesses = 0;
305
397
  const now = this.now();
306
398
  lane.cooldownUntil = Math.max(
@@ -348,6 +440,7 @@ export class ProviderAdmissionScheduler {
348
440
  lane.cooldownUntil = 0;
349
441
  lane.limit = this.concurrency;
350
442
  lane.recoverySuccesses = 0;
443
+ lane.recoveryTarget = null;
351
444
  if (lane.cooldownTimer) {
352
445
  this.clearTimer(lane.cooldownTimer);
353
446
  lane.cooldownTimer = null;
@@ -364,11 +457,15 @@ export class ProviderAdmissionScheduler {
364
457
  }
365
458
 
366
459
  _recordSuccess(lane) {
367
- if (!lane.adaptive || lane.limit >= this.concurrency) return;
460
+ if (!lane.adaptive || !Number.isFinite(lane.limit)) return;
368
461
  lane.recoverySuccesses += 1;
369
462
  if (lane.recoverySuccesses >= lane.limit) {
370
463
  lane.limit += 1;
371
464
  lane.recoverySuccesses = 0;
465
+ if (lane.recoveryTarget && lane.limit >= lane.recoveryTarget) {
466
+ lane.limit = this.concurrency;
467
+ lane.recoveryTarget = null;
468
+ }
372
469
  }
373
470
  }
374
471
 
@@ -505,6 +602,8 @@ export function wrapProviderAdmission(provider, providerName, scheduler = provid
505
602
  });
506
603
  }, {
507
604
  signal,
605
+ ownerKey: opts.admissionOwner || opts.sessionId || null,
606
+ priority: opts.admissionPriority || 'user-visible',
508
607
  onCooldownWait: (waitMs) => {
509
608
  // Display-only: the TUI/desktop 'reconnecting' stage already
510
609
  // renders a custom verb, so no new stage vocabulary is needed.
@@ -23,6 +23,7 @@ import {
23
23
  NATIVE_SERVER_TOOL_CALL_BLOCK_TYPES,
24
24
  NATIVE_SERVER_TOOL_RESULT_BLOCK_TYPES,
25
25
  } from './lib/anthropic-native-blocks.mjs';
26
+ import { parseProviderJsonBatch } from './stream-json-pool.mjs';
26
27
  import { stampStreamOutcome, STREAM_TRANSPORTS, STREAM_OUTCOME_VERSION } from './lib/stream-outcome.mjs';
27
28
 
28
29
  /** Bounded mid-stream SSE retries (transient stream loss); shared with anthropic.mjs.
@@ -486,7 +487,10 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
486
487
  if (!data) continue;
487
488
 
488
489
  try {
489
- const event = JSON.parse(data);
490
+ // A completed reader.read() owns this frame even if abort
491
+ // races immediately afterward; the next read observes the
492
+ // cancellation after this frame is relayed.
493
+ const [event] = await parseProviderJsonBatch([data]);
490
494
 
491
495
  // SEMANTIC idle reset (Part A): reset the idle timer ONLY for
492
496
  // real progress events, NOT for Anthropic keepalives. Anthropic
@@ -23,6 +23,7 @@ import {
23
23
  collectGeminiGroundingSources,
24
24
  parseGeminiTextPartMetadata,
25
25
  } from './gemini-schema.mjs';
26
+ import { parseProviderJsonBatch } from './stream-json-pool.mjs';
26
27
 
27
28
  export const GEMINI_FIRST_BYTE_TIMEOUT_MS = resolveTimeoutMs(
28
29
  'MIXDOG_GEMINI_FIRST_BYTE_TIMEOUT_MS',
@@ -131,6 +132,19 @@ function geminiChunkHasFunctionCall(chunk) {
131
132
  return parts.some((p) => p && p.functionCall);
132
133
  }
133
134
 
135
+ export function geminiChunkProgressKind(chunk) {
136
+ const parts = chunk?.candidates?.[0]?.content?.parts;
137
+ if (!Array.isArray(parts)) return 'transport';
138
+ if (parts.some((part) => part?.functionCall)) return 'tool';
139
+ if (parts.some((part) => part?.thought === true && typeof part.text === 'string' && part.text)) {
140
+ return 'reasoning';
141
+ }
142
+ if (parts.some((part) => part?.thought !== true && typeof part?.text === 'string' && part.text)) {
143
+ return 'text';
144
+ }
145
+ return 'transport';
146
+ }
147
+
134
148
  /**
135
149
  * Aggregate streamed GenerateContentResponse chunks into one response object
136
150
  * (same shape as a non-streaming generateContent JSON body).
@@ -256,7 +270,7 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
256
270
  };
257
271
  leakedCalls.push(call);
258
272
  try { onToolCall?.(call); } catch {}
259
- try { onStreamDelta?.(); } catch {}
273
+ try { onStreamDelta?.('tool'); } catch {}
260
274
  };
261
275
 
262
276
  const pumpLeakBuffer = (final) => {
@@ -407,6 +421,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
407
421
  if (done) break;
408
422
  resetIdleTimer();
409
423
  buffer += decoder.decode(value, { stream: true });
424
+ const payloads = [];
410
425
  let lineEnd;
411
426
  while ((lineEnd = buffer.indexOf('\n')) >= 0) {
412
427
  let line = buffer.slice(0, lineEnd);
@@ -415,18 +430,24 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
415
430
  if (!line.startsWith('data: ')) continue;
416
431
  const data = line.slice(6).trim();
417
432
  if (!data || data === '[DONE]') continue;
418
- let parsed;
419
- try {
420
- parsed = JSON.parse(data);
421
- } catch (cause) {
422
- throw geminiStreamCorruptionError(`${label} corrupt SSE JSON`, cause);
423
- }
433
+ payloads.push(data);
434
+ }
435
+ let parsedChunks;
436
+ try {
437
+ // Preserve the established delivery boundary: once read()
438
+ // returned the bytes, parse and relay them before the next
439
+ // iteration observes cancellation.
440
+ parsedChunks = await parseProviderJsonBatch(payloads);
441
+ } catch (cause) {
442
+ throw geminiStreamCorruptionError(`${label} corrupt SSE JSON`, cause);
443
+ }
444
+ for (const parsed of parsedChunks) {
424
445
  if (!sawStreamChunk) {
425
446
  sawStreamChunk = true;
426
447
  clearFirstByteTimer();
427
448
  }
428
449
  allChunks.push(parsed);
429
- try { onStreamDelta?.(); } catch {}
450
+ try { onStreamDelta?.(geminiChunkProgressKind(parsed)); } catch {}
430
451
  if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
431
452
  if (onTextDelta || textLeakGuard) {
432
453
  const t = geminiChunkText(parsed);
@@ -440,13 +461,13 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
440
461
  const data = line.slice(6).trim();
441
462
  if (data && data !== '[DONE]') {
442
463
  try {
443
- const parsed = JSON.parse(data);
464
+ const [parsed] = await parseProviderJsonBatch([data]);
444
465
  if (!sawStreamChunk) {
445
466
  sawStreamChunk = true;
446
467
  clearFirstByteTimer();
447
468
  }
448
469
  allChunks.push(parsed);
449
- try { onStreamDelta?.(); } catch {}
470
+ try { onStreamDelta?.(geminiChunkProgressKind(parsed)); } catch {}
450
471
  if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
451
472
  if (onTextDelta || textLeakGuard) {
452
473
  const t = geminiChunkText(parsed);
@@ -677,7 +698,7 @@ export async function consumeGeminiSdkStream(streamResult, {
677
698
  }
678
699
  resetIdleTimer();
679
700
  if (step.value) collectedChunks.push(step.value);
680
- try { onStreamDelta?.(); } catch {}
701
+ try { onStreamDelta?.(geminiChunkProgressKind(step.value)); } catch {}
681
702
  if (!sawFunctionCall && geminiChunkHasFunctionCall(step.value)) sawFunctionCall = true;
682
703
  if (onTextDelta || textLeakGuard) {
683
704
  const t = geminiChunkText(step.value);
@@ -1,6 +1,6 @@
1
1
  import { GoogleGenerativeAI } from '@google/generative-ai';
2
2
  import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
3
- import { withRetry } from './retry-classifier.mjs';
3
+ import { canFallbackNonStreaming, withRetry } from './retry-classifier.mjs';
4
4
  import { traceAgentUsage, appendAgentTrace } from '../agent-trace.mjs';
5
5
  import {
6
6
  PROVIDER_FIRST_BYTE_TIMEOUT_MS,
@@ -158,6 +158,53 @@ export class GeminiProvider {
158
158
  return this.config?.apiKey || process.env.GEMINI_API_KEY || '';
159
159
  }
160
160
 
161
+ /**
162
+ * cc parity (claude.ts:2504): re-issue a dead stream ONCE as a
163
+ * non-streaming generateContent call instead of failing the turn.
164
+ * Narrower than cc by design — canFallbackNonStreaming() clears only a
165
+ * stream that exposed nothing, so rendered text is never duplicated and a
166
+ * dispatched tool can never run twice. Returns the aggregated response, or
167
+ * null when the failure is ineligible or the fallback itself fails.
168
+ */
169
+ async _recoverGeminiNonStreaming({ streamErr, signal, opts, model, generate }) {
170
+ if (!canFallbackNonStreaming(streamErr, { signal })) return null;
171
+ let aggregated;
172
+ try {
173
+ try { opts?.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
174
+ aggregated = await generate(signal || undefined);
175
+ } catch {
176
+ return null;
177
+ }
178
+ if (!aggregated || !Array.isArray(aggregated.candidates) || aggregated.candidates.length === 0) {
179
+ return null;
180
+ }
181
+ try {
182
+ process.stderr.write(
183
+ `[gemini] stream failed (${streamErr?.code || streamErr?.message || 'unknown'}); `
184
+ + `recovered via non-streaming generateContent\n`,
185
+ );
186
+ } catch { /* best-effort */ }
187
+ try {
188
+ appendAgentTrace({
189
+ sessionId: opts?.sessionId || opts?.session?.id || null,
190
+ iteration: Number.isFinite(Number(opts?.iteration)) ? Number(opts.iteration) : null,
191
+ kind: 'transport_fallback',
192
+ provider: 'gemini',
193
+ model,
194
+ transport: 'non-streaming',
195
+ payload: {
196
+ from: 'stream',
197
+ to: 'non-streaming',
198
+ reason: streamErr?.retryClassifier || streamErr?.code || streamErr?.message || 'stream_failed',
199
+ error_code: streamErr?.code || null,
200
+ error_http_status: Number(streamErr?.httpStatus || streamErr?.status || 0) || null,
201
+ error_classifier: streamErr?.retryClassifier || streamErr?.midstreamClassifier || null,
202
+ },
203
+ });
204
+ } catch { /* best-effort */ }
205
+ return aggregated;
206
+ }
207
+
161
208
  // Explicit cachedContents API. The implicit cache layer on Gemini 3.x
162
209
  // does not surface cachedContentTokenCount in usageMetadata, so the only
163
210
  // way to obtain measurable + billable cache savings is to register the
@@ -655,7 +702,30 @@ export class GeminiProvider {
655
702
  }
656
703
  return await this._doSend(messages, model, tools, opts, { skipExplicitCache: true });
657
704
  }
658
- throw err;
705
+ const recovered = await this._recoverGeminiNonStreaming({
706
+ streamErr: err,
707
+ signal: totalSignal,
708
+ opts,
709
+ model: useModel,
710
+ generate: async (fallbackSignal) => {
711
+ const nonStreamUrl = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(useModel)}:generateContent?key=${encodeURIComponent(apiKey)}`;
712
+ const res = await this._fetch(nonStreamUrl, {
713
+ method: 'POST',
714
+ headers: { 'Content-Type': 'application/json' },
715
+ body: JSON.stringify(body),
716
+ signal: fallbackSignal,
717
+ dispatcher: getLlmDispatcher(),
718
+ });
719
+ if (!res.ok) {
720
+ const text = await res.text().catch(() => '');
721
+ throw geminiRestError(res, text, 'Gemini REST generateContent');
722
+ }
723
+ return await res.json();
724
+ },
725
+ });
726
+ if (!recovered) throw err;
727
+ textLeakGuard = null;
728
+ response = recovered;
659
729
  } finally {
660
730
  restPassthrough.cleanup();
661
731
  }
@@ -749,6 +819,23 @@ export class GeminiProvider {
749
819
  },
750
820
  },
751
821
  );
822
+ } catch (err) {
823
+ const recovered = await this._recoverGeminiNonStreaming({
824
+ streamErr: err,
825
+ signal: totalSignal,
826
+ opts,
827
+ model: useModel,
828
+ generate: async (fallbackSignal) => {
829
+ const result = await genModel.generateContent(
830
+ { contents },
831
+ { signal: fallbackSignal },
832
+ );
833
+ return result?.response ?? null;
834
+ },
835
+ });
836
+ if (!recovered) throw err;
837
+ textLeakGuard = null;
838
+ response = recovered;
752
839
  } finally {
753
840
  sdkPassthrough.cleanup();
754
841
  }
@@ -23,7 +23,7 @@ import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs
23
23
  import { enrichModels, getModelMetadataSync } from './model-catalog.mjs';
24
24
  import { sanitizeModelList } from './model-list-sanitize.mjs';
25
25
  import { makeModelCache } from './model-cache.mjs';
26
- import { OpenAICompatProvider } from './openai-compat.mjs';
26
+ import { OpenAICompatProvider, preloadOpenAICompatRuntime } from './openai-compat.mjs';
27
27
  import { createTimeoutSignal } from '../stall-policy.mjs';
28
28
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
29
29
  import { normalizeGrokToolSchemas } from './lib/grok-tool-schema.mjs';
@@ -205,6 +205,17 @@ export class GrokOAuthProvider {
205
205
  constructor(config) {
206
206
  this.config = config || {};
207
207
  this.tokens = loadTokens();
208
+ // The credential/header-bound inner provider is created lazily, but its
209
+ // heavy SDK + undici graph and proxy socket are not request-specific.
210
+ // Pay both during the daemon's process-wide provider warmup so first
211
+ // Grok send only builds the cheap inner client.
212
+ try { preloadOpenAICompatRuntime(); } catch { /* authoritative send surfaces load failures */ }
213
+ if (this.config?.preconnect !== false) {
214
+ const warm = typeof this.config?.preconnectFn === 'function'
215
+ ? this.config.preconnectFn
216
+ : preconnect;
217
+ try { warm(PROXY_BASE_URL); } catch { /* best-effort */ }
218
+ }
208
219
  }
209
220
 
210
221
  async ensureAuth({ forceRefresh = false } = {}) {
@@ -59,6 +59,22 @@ export function xaiResponsesCacheRouting(opts, params, rawTools, model) {
59
59
  const scope = String(opts?.xaiResponsesCacheScope || process.env.MIXDOG_XAI_RESPONSES_CACHE_SCOPE || 'prefix')
60
60
  .trim()
61
61
  .toLowerCase();
62
+ // 'none' omits prompt_cache_key entirely, matching xAI's own reference
63
+ // sampler (grok-build sends prompt_cache_key: None) and zed's xAI provider
64
+ // (supports_prompt_cache_key() == false): the service falls back to
65
+ // automatic prompt-prefix caching with no client-supplied lane. Callers
66
+ // must treat a null key as "do not send the field".
67
+ if (scope === 'none' || scope === 'off' || scope === 'omit') {
68
+ const sessionId = String(opts?.sessionId || opts?.session?.id || '').trim();
69
+ const prefixHash = traceHash(xaiPrefixSeed({ opts, params, rawTools, model }));
70
+ return {
71
+ key: null,
72
+ mode: 'none',
73
+ seedHash: null,
74
+ prefixHash,
75
+ ownerSessionHash: sessionId ? traceHash(sessionId) : null,
76
+ };
77
+ }
62
78
  if (scope !== 'prefix') {
63
79
  return xaiCacheRouting(opts, params, rawTools, model);
64
80
  }
@@ -183,7 +199,9 @@ export function useXaiResponsesWebSocketWarmup(opts, config, { previousResponseI
183
199
  if (['0', 'false', 'off', 'none', 'disabled'].includes(mode)) return false;
184
200
  if (['1', 'true', 'on', 'always', 'force'].includes(mode)) return true;
185
201
  }
186
- return String(instructions || '').length >= 2048 || (Array.isArray(rawTools) && rawTools.length >= 10);
202
+ // A generate:false response can still carry provider usage/quota. Keep it
203
+ // strictly opt-in; transport-only HTTP/WS preconnect remains cost-free.
204
+ return false;
187
205
  }
188
206
 
189
207
  // Codex-aligned default: no compat cache lane/serialization. Keep the override