mixdog 0.8.0 → 0.9.0

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 (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OpenAI Codex OAuth — WebSocket transport.
2
+ * OpenAI OAuth subscription — WebSocket transport.
3
3
  *
4
4
  * Single dispatch path for the openai-oauth provider (SSE removed in
5
5
  * v0.6.117). Uses the `responses_websockets=2026-02-06` beta WebSocket
@@ -28,12 +28,13 @@ import { errText } from '../../../shared/err-text.mjs';
28
28
  import { createHash, randomBytes } from 'crypto';
29
29
  import {
30
30
  extractCachedTokens,
31
- traceBridgeFetch,
32
- traceBridgeSse,
33
- traceBridgeUsage,
34
- appendBridgeTrace,
35
- } from '../bridge-trace.mjs';
31
+ traceAgentFetch,
32
+ traceAgentSse,
33
+ traceAgentUsage,
34
+ appendAgentTrace,
35
+ } from '../agent-trace.mjs';
36
36
  import { jitterDelayMs, populateHttpStatusFromMessage } from './retry-classifier.mjs';
37
+ import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
37
38
  import {
38
39
  PROVIDER_RETRY_MAX_ATTEMPTS,
39
40
  PROVIDER_WS_ACQUIRE_TIMEOUT_MS,
@@ -41,6 +42,7 @@ import {
41
42
  PROVIDER_WS_HANDSHAKE_TIMEOUT_MS,
42
43
  PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS,
43
44
  } from '../stall-policy.mjs';
45
+ import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
44
46
 
45
47
  globalThis.__mixdogOpenaiWsRuntimeLoaded = true;
46
48
 
@@ -69,9 +71,9 @@ const WS_PRE_RESPONSE_CREATED_MS = (() => {
69
71
  })();
70
72
  // Inter-chunk inactivity after first meaningful output.
71
73
  const WS_INTER_CHUNK_MS = PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS;
72
- const MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT = 2;
73
- const MIDSTREAM_DEFAULT_RETRY_LIMIT = 1;
74
- const MIDSTREAM_BACKOFF_MS = [250, 1000];
74
+ const MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT = 4;
75
+ const MIDSTREAM_DEFAULT_RETRY_LIMIT = 2;
76
+ const MIDSTREAM_BACKOFF_MS = [250, 1000, 2000, 4000];
75
77
 
76
78
  // Handshake retry policy. The `ws` library surfaces a bare
77
79
  // `Opening handshake has timed out` Error after handshakeTimeout; transient
@@ -88,14 +90,14 @@ const HANDSHAKE_MAX_ATTEMPTS = PROVIDER_RETRY_MAX_ATTEMPTS;
88
90
  const HANDSHAKE_BACKOFF_BASE_MS = 500;
89
91
  const HANDSHAKE_BACKOFF_CAP_MS = 5000;
90
92
  // WS socket pool buckets are keyed by `poolKey` (the per-call sessionId)
91
- // to isolate parallel bridge invocations — each gets its own socket so
92
- // a second caller cannot grab a sibling's mid-turn entry (Codex would
93
+ // to isolate parallel agent invocations — each gets its own socket so
94
+ // a second caller cannot grab a sibling's mid-turn entry (openai-oauth would
93
95
  // otherwise reject the new response.create with "No tool output found
94
- // for function call ..."). The Codex handshake `session_id` header/URL
96
+ // for function call ..."). The handshake `session_id` header/URL
95
97
  // uses `cacheKey` — a prefix-scoped cache key derived from the configured
96
98
  // provider namespace plus model/system/tools hash. Same-prefix sessions share
97
99
  // server-side prompt cache, while unrelated main/worker prefixes no longer
98
- // evict each other inside one static provider lane. Codex dedupes cache by
100
+ // evict each other inside one static provider lane. The backend dedupes cache by
99
101
  // handshake session_id, not by body.prompt_cache_key alone (measured
100
102
  // 2026-04-19 after the v0.6.151 regression).
101
103
  const MAX_POOLED_SOCKETS_PER_KEY = 8;
@@ -106,10 +108,10 @@ const MAX_POOLED_SOCKETS_PER_KEY = 8;
106
108
  // closing, ephemeral }
107
109
  const _wsPool = new Map();
108
110
 
109
- // Final prompt_cache_key/session_id lane guard for OpenAI/Codex transports.
111
+ // Final prompt_cache_key/session_id lane guard for OpenAI OAuth transports.
110
112
  // The provider code may shard one logical prefix into N cache keys for 10+
111
113
  // total parallelism; inside each final key we still serialize requests because
112
- // live Codex probes show same-key concurrent WebSockets can randomly miss the
114
+ // Live probes show same-key concurrent WebSockets can randomly miss the
113
115
  // server prompt cache even after warm-up.
114
116
  const _openAiPromptCacheLanes = new Map();
115
117
  const _openAiPromptCacheLaneRates = new Map();
@@ -121,6 +123,11 @@ function _positiveInt(value, fallback = 0) {
121
123
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
122
124
  }
123
125
 
126
+ function _nonNegativeInt(value, fallback = 0) {
127
+ const n = Number(value);
128
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
129
+ }
130
+
124
131
  function _cacheLaneHash(value) {
125
132
  return createHash('sha256').update(String(value || '')).digest('hex').slice(0, 12);
126
133
  }
@@ -146,7 +153,7 @@ function _openAiPromptCacheLaneQueueTimeoutMs(sendOpts = {}) {
146
153
  }
147
154
 
148
155
  function _openAiPromptCacheLaneRateLimitPerMin(sendOpts = {}) {
149
- return _positiveInt(
156
+ return _nonNegativeInt(
150
157
  sendOpts?.openaiCacheLaneRateLimitPerMin
151
158
  ?? sendOpts?.promptCacheLaneRateLimitPerMin
152
159
  ?? process.env.MIXDOG_OPENAI_CACHE_LANE_RPM
@@ -155,6 +162,45 @@ function _openAiPromptCacheLaneRateLimitPerMin(sendOpts = {}) {
155
162
  );
156
163
  }
157
164
 
165
+ function _openAiPromptCacheLaneDeltaRateLimitPerMin(sendOpts = {}) {
166
+ return _nonNegativeInt(
167
+ sendOpts?.openaiCacheLaneDeltaRateLimitPerMin
168
+ ?? sendOpts?.promptCacheLaneDeltaRateLimitPerMin
169
+ ?? process.env.MIXDOG_OPENAI_CACHE_LANE_DELTA_RPM
170
+ ?? process.env.MIXDOG_OPENAI_CACHE_DELTA_RPM,
171
+ 60,
172
+ );
173
+ }
174
+
175
+ function _openAiPromptCacheLaneDeltaMaxItems(sendOpts = {}) {
176
+ return _nonNegativeInt(
177
+ sendOpts?.openaiCacheLaneDeltaMaxItems
178
+ ?? sendOpts?.promptCacheLaneDeltaMaxItems
179
+ ?? process.env.MIXDOG_OPENAI_CACHE_LANE_DELTA_MAX_ITEMS
180
+ ?? process.env.MIXDOG_OPENAI_CACHE_DELTA_MAX_ITEMS,
181
+ 8,
182
+ );
183
+ }
184
+
185
+ function _openAiPromptCacheLaneDeltaMaxTokens(sendOpts = {}) {
186
+ return _nonNegativeInt(
187
+ sendOpts?.openaiCacheLaneDeltaMaxTokens
188
+ ?? sendOpts?.promptCacheLaneDeltaMaxTokens
189
+ ?? process.env.MIXDOG_OPENAI_CACHE_LANE_DELTA_MAX_TOKENS
190
+ ?? process.env.MIXDOG_OPENAI_CACHE_DELTA_MAX_TOKENS,
191
+ 20_000,
192
+ );
193
+ }
194
+
195
+ function _openAiPromptCacheLaneSlowTraceMs(sendOpts = {}) {
196
+ return _positiveInt(
197
+ sendOpts?.openaiCacheLaneSlowTraceMs
198
+ ?? sendOpts?.promptCacheLaneSlowTraceMs
199
+ ?? process.env.MIXDOG_OPENAI_CACHE_LANE_SLOW_MS,
200
+ 3000,
201
+ );
202
+ }
203
+
158
204
  function _isOpenAiPromptCacheLaneAuth(auth) {
159
205
  return auth?.type !== 'xai';
160
206
  }
@@ -229,12 +275,13 @@ function _sleepWithSignal(ms, signal) {
229
275
  });
230
276
  }
231
277
 
232
- async function _reserveOpenAiPromptCacheLaneRate({ key, limitPerMin, signal }) {
278
+ async function _reserveOpenAiPromptCacheLaneRate({ key, limitPerMin, signal, beforeWait }) {
233
279
  if (!limitPerMin || limitPerMin <= 0) {
234
280
  return { rateLimitPerMin: 0, rateWaitMs: 0, rateWindowCount: 0 };
235
281
  }
236
282
  const state = _getOpenAiPromptCacheLaneRateState(key);
237
283
  const startedAt = Date.now();
284
+ let beforeWaitCalled = false;
238
285
  while (true) {
239
286
  const now = Date.now();
240
287
  _pruneOpenAiPromptCacheLaneRateState(state, now);
@@ -247,10 +294,57 @@ async function _reserveOpenAiPromptCacheLaneRate({ key, limitPerMin, signal }) {
247
294
  };
248
295
  }
249
296
  const waitMs = Math.max(25, state.starts[0] + OPENAI_PROMPT_CACHE_LANE_RATE_WINDOW_MS - now);
297
+ if (!beforeWaitCalled && typeof beforeWait === 'function') {
298
+ beforeWaitCalled = true;
299
+ await beforeWait({
300
+ waitMs,
301
+ rateLimitPerMin: limitPerMin,
302
+ rateWindowCount: state.starts.length,
303
+ });
304
+ }
250
305
  await _sleepWithSignal(waitMs, signal);
251
306
  }
252
307
  }
253
308
 
309
+ export function _resolveOpenAiPromptCacheRatePolicy(sendOpts = {}, info = {}) {
310
+ const mode = String(info?.mode || '').toLowerCase();
311
+ const frameInputItems = Number(info?.frameInputItems);
312
+ const deltaTokens = Number(info?.deltaTokens);
313
+ const hasPreviousResponseId = info?.hasPreviousResponseId === true;
314
+ const fullLimitPerMin = _openAiPromptCacheLaneRateLimitPerMin(sendOpts);
315
+ const deltaLimitPerMin = _openAiPromptCacheLaneDeltaRateLimitPerMin(sendOpts);
316
+ const deltaMaxItems = _openAiPromptCacheLaneDeltaMaxItems(sendOpts);
317
+ const deltaMaxTokens = _openAiPromptCacheLaneDeltaMaxTokens(sendOpts);
318
+ const itemCount = Number.isFinite(frameInputItems) ? frameInputItems : null;
319
+ const tokenCount = Number.isFinite(deltaTokens) ? deltaTokens : null;
320
+ const smallDeltaItems = itemCount == null || deltaMaxItems <= 0 || itemCount <= deltaMaxItems;
321
+ const smallDeltaTokens = tokenCount == null || deltaMaxTokens <= 0 || tokenCount <= deltaMaxTokens;
322
+
323
+ if (mode === 'delta' && hasPreviousResponseId && smallDeltaItems && smallDeltaTokens) {
324
+ return {
325
+ policy: deltaLimitPerMin > 0 ? 'delta_relaxed' : 'delta_unlimited',
326
+ limitPerMin: deltaLimitPerMin,
327
+ fullLimitPerMin,
328
+ deltaLimitPerMin,
329
+ deltaMaxItems,
330
+ deltaMaxTokens,
331
+ frameInputItems: itemCount,
332
+ deltaTokens: tokenCount,
333
+ };
334
+ }
335
+
336
+ return {
337
+ policy: mode === 'delta' ? 'delta_guarded' : 'full_guard',
338
+ limitPerMin: fullLimitPerMin,
339
+ fullLimitPerMin,
340
+ deltaLimitPerMin,
341
+ deltaMaxItems,
342
+ deltaMaxTokens,
343
+ frameInputItems: itemCount,
344
+ deltaTokens: tokenCount,
345
+ };
346
+ }
347
+
254
348
  function _removeQueuedOpenAiPromptCacheLaneRequest(state, request) {
255
349
  const index = state.queue.indexOf(request);
256
350
  if (index >= 0) state.queue.splice(index, 1);
@@ -332,29 +426,10 @@ async function _withOpenAiPromptCacheLane({ auth, cacheKey, sendOpts, poolKey, i
332
426
  }
333
427
  const laneKey = `openai-prompt:${traceProvider || 'openai'}:${useModel || 'default'}:${cacheKey}`;
334
428
  const laneKeyHash = _cacheLaneHash(laneKey);
335
- const rateMeta = await _reserveOpenAiPromptCacheLaneRate({
336
- key: laneKey,
337
- limitPerMin: _openAiPromptCacheLaneRateLimitPerMin(sendOpts),
338
- signal: externalSignal,
339
- });
340
- if (rateMeta.rateWaitMs > 0) {
341
- appendBridgeTrace({
342
- sessionId: poolKey,
343
- iteration,
344
- kind: 'cache_lane',
345
- provider: traceProvider,
346
- model: useModel,
347
- event: 'rate_wait',
348
- lane_key_hash: laneKeyHash,
349
- rate_limit_per_min: rateMeta.rateLimitPerMin,
350
- rate_wait_ms: rateMeta.rateWaitMs,
351
- rate_window_count: rateMeta.rateWindowCount,
352
- });
353
- }
354
429
  const state = _getOpenAiPromptCacheLaneState(laneKey, maxInFlight);
355
430
  const queued = state.active >= state.maxInFlight;
356
431
  if (queued) {
357
- appendBridgeTrace({
432
+ appendAgentTrace({
358
433
  sessionId: poolKey,
359
434
  iteration,
360
435
  kind: 'cache_lane',
@@ -368,23 +443,139 @@ async function _withOpenAiPromptCacheLane({ auth, cacheKey, sendOpts, poolKey, i
368
443
  });
369
444
  }
370
445
  const timeoutMs = _openAiPromptCacheLaneQueueTimeoutMs(sendOpts);
371
- const handle = await _acquireOpenAiPromptCacheLane({ key: laneKey, maxInFlight, signal: externalSignal, timeoutMs });
446
+ let handle = await _acquireOpenAiPromptCacheLane({ key: laneKey, maxInFlight, signal: externalSignal, timeoutMs });
447
+ let handleActive = true;
372
448
  const laneMeta = {
373
449
  enabled: true,
374
450
  laneKeyHash,
375
451
  maxInFlight,
376
- rateLimitPerMin: rateMeta.rateLimitPerMin,
377
- rateWaitMs: rateMeta.rateWaitMs,
378
- rateWindowCount: rateMeta.rateWindowCount,
452
+ ratePolicy: 'pending',
453
+ rateLimitPerMin: 0,
454
+ rateWaitMs: 0,
455
+ rateWindowCount: 0,
456
+ rateReleasedForWait: false,
457
+ rateReacquireWaitMs: 0,
379
458
  queued: queued || handle.queued === true,
380
459
  waitMs: handle.waitedMs,
381
460
  activeAfterAcquire: handle.activeCount,
382
461
  queueDepthAfterAcquire: handle.queueDepth,
462
+ async reserveRate(info = {}) {
463
+ if (laneMeta.ratePolicy !== 'pending') return laneMeta;
464
+ const policy = _resolveOpenAiPromptCacheRatePolicy(sendOpts, info);
465
+ let releasedForRateWait = false;
466
+ const rateMeta = await _reserveOpenAiPromptCacheLaneRate({
467
+ key: laneKey,
468
+ limitPerMin: policy.limitPerMin,
469
+ signal: externalSignal,
470
+ beforeWait: () => {
471
+ if (!handleActive) return;
472
+ handle.release();
473
+ handleActive = false;
474
+ releasedForRateWait = true;
475
+ },
476
+ });
477
+ let reacquireWaitMs = 0;
478
+ if (releasedForRateWait) {
479
+ const reacquired = await _acquireOpenAiPromptCacheLane({
480
+ key: laneKey,
481
+ maxInFlight,
482
+ signal: externalSignal,
483
+ timeoutMs,
484
+ });
485
+ handle = reacquired;
486
+ handleActive = true;
487
+ reacquireWaitMs = reacquired.waitedMs;
488
+ laneMeta.queued = laneMeta.queued || reacquired.queued === true;
489
+ laneMeta.waitMs = (Number(laneMeta.waitMs) || 0) + reacquireWaitMs;
490
+ laneMeta.activeAfterAcquire = reacquired.activeCount;
491
+ laneMeta.queueDepthAfterAcquire = reacquired.queueDepth;
492
+ }
493
+ Object.assign(laneMeta, {
494
+ ratePolicy: policy.policy,
495
+ rateLimitPerMin: rateMeta.rateLimitPerMin,
496
+ rateWaitMs: rateMeta.rateWaitMs,
497
+ rateWindowCount: rateMeta.rateWindowCount,
498
+ rateReleasedForWait: releasedForRateWait,
499
+ rateReacquireWaitMs: reacquireWaitMs,
500
+ rateFullLimitPerMin: policy.fullLimitPerMin,
501
+ rateDeltaLimitPerMin: policy.deltaLimitPerMin,
502
+ rateDeltaMaxItems: policy.deltaMaxItems,
503
+ rateDeltaMaxTokens: policy.deltaMaxTokens,
504
+ ratePolicyFrameInputItems: policy.frameInputItems,
505
+ ratePolicyDeltaTokens: policy.deltaTokens,
506
+ });
507
+ if (rateMeta.rateWaitMs > 0) {
508
+ appendAgentTrace({
509
+ sessionId: poolKey,
510
+ iteration,
511
+ kind: 'cache_lane',
512
+ provider: traceProvider,
513
+ model: useModel,
514
+ event: 'rate_wait',
515
+ lane_key_hash: laneKeyHash,
516
+ rate_policy: laneMeta.ratePolicy,
517
+ rate_limit_per_min: rateMeta.rateLimitPerMin,
518
+ rate_wait_ms: rateMeta.rateWaitMs,
519
+ rate_window_count: rateMeta.rateWindowCount,
520
+ released_for_rate_wait: releasedForRateWait,
521
+ reacquire_wait_ms: reacquireWaitMs,
522
+ frame_input_items: policy.frameInputItems,
523
+ delta_tokens: policy.deltaTokens,
524
+ });
525
+ }
526
+ return laneMeta;
527
+ },
383
528
  };
384
529
  try {
385
530
  return await fn(laneMeta);
386
531
  } finally {
387
- handle.release();
532
+ const slowTraceMs = _openAiPromptCacheLaneSlowTraceMs(sendOpts);
533
+ const slowWaitMs = Math.max(Number(laneMeta.rateWaitMs) || 0, Number(laneMeta.waitMs) || 0);
534
+ if (slowTraceMs > 0 && slowWaitMs >= slowTraceMs) {
535
+ appendAgentTrace({
536
+ sessionId: poolKey,
537
+ iteration,
538
+ kind: 'cache_lane_slow',
539
+ provider: traceProvider,
540
+ model: useModel,
541
+ event: laneMeta.rateWaitMs > 0 && laneMeta.waitMs > 0
542
+ ? 'rate_and_queue_wait'
543
+ : laneMeta.rateWaitMs > 0
544
+ ? 'rate_wait'
545
+ : 'queue_wait',
546
+ lane_key_hash: laneKeyHash,
547
+ payload: {
548
+ event: laneMeta.rateWaitMs > 0 && laneMeta.waitMs > 0
549
+ ? 'rate_and_queue_wait'
550
+ : laneMeta.rateWaitMs > 0
551
+ ? 'rate_wait'
552
+ : 'queue_wait',
553
+ provider: traceProvider,
554
+ model: useModel,
555
+ lane_key_hash: laneKeyHash,
556
+ threshold_ms: slowTraceMs,
557
+ max_wait_ms: slowWaitMs,
558
+ rate_policy: laneMeta.ratePolicy,
559
+ rate_limit_per_min: laneMeta.rateLimitPerMin,
560
+ rate_wait_ms: laneMeta.rateWaitMs,
561
+ rate_window_count: laneMeta.rateWindowCount,
562
+ released_for_rate_wait: laneMeta.rateReleasedForWait,
563
+ reacquire_wait_ms: laneMeta.rateReacquireWaitMs,
564
+ rate_full_limit_per_min: laneMeta.rateFullLimitPerMin,
565
+ rate_delta_limit_per_min: laneMeta.rateDeltaLimitPerMin,
566
+ rate_delta_max_items: laneMeta.rateDeltaMaxItems,
567
+ rate_delta_max_tokens: laneMeta.rateDeltaMaxTokens,
568
+ rate_policy_frame_input_items: laneMeta.ratePolicyFrameInputItems,
569
+ rate_policy_delta_tokens: laneMeta.ratePolicyDeltaTokens,
570
+ max_in_flight: laneMeta.maxInFlight,
571
+ queued: laneMeta.queued,
572
+ wait_ms: laneMeta.waitMs,
573
+ active_after_acquire: laneMeta.activeAfterAcquire,
574
+ queue_depth_after_acquire: laneMeta.queueDepthAfterAcquire,
575
+ },
576
+ });
577
+ }
578
+ if (handleActive) handle.release();
388
579
  }
389
580
  }
390
581
 
@@ -503,14 +694,14 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cach
503
694
  return headers;
504
695
  }
505
696
 
506
- // handshake session_id is the conversation slot Codex uses for in-memory
697
+ // handshake session_id is the conversation slot openai-oauth uses for in-memory
507
698
  // prefix state. All orchestrator-internal dispatches for this provider share
508
699
  // the same cacheKey (built in manager.mjs via providerCacheKey()), so they
509
700
  // share the server-side prefix-cache shard across roles/sources.
510
701
  function _mintSessionToken(cacheKey, auth) {
511
702
  // xAI's public WebSocket endpoint uses the open connection plus
512
- // response ids for continuation; unlike Codex, it does not need the
513
- // Codex-specific session_id handshake shard.
703
+ // response ids for continuation; unlike openai-oauth, it does not need the
704
+ // OAuth-specific session_id handshake shard.
514
705
  if (auth?.type === 'xai') return null;
515
706
  return cacheKey || 'mixdog-default';
516
707
  }
@@ -523,8 +714,8 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }
523
714
  ? OPENAI_WS_URL
524
715
  : CODEX_WS_URL;
525
716
  const _wsOpenStart = Date.now();
526
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
527
- process.stderr.write(`[bridge-trace] ws-open-start url=${baseUrl} tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} ts=${_wsOpenStart}\n`);
717
+ if (process.env.MIXDOG_DEBUG_AGENT) {
718
+ process.stderr.write(`[agent-trace] ws-open-start url=${baseUrl} tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} ts=${_wsOpenStart}\n`);
528
719
  }
529
720
  const url = baseUrl + (sessionToken ? `?session_id=${encodeURIComponent(String(sessionToken))}` : '');
530
721
  return new Promise((resolve, reject) => {
@@ -546,8 +737,8 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }
546
737
  const socket = new WebSocket(url, { headers, handshakeTimeout: WS_HANDSHAKE_TIMEOUT_MS });
547
738
  acquireTimer = setTimeout(() => {
548
739
  if (settled) return;
549
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
550
- process.stderr.write(`[bridge-trace] ws-open-fail kind=acquire_timeout timeoutMs=${WS_ACQUIRE_TIMEOUT_MS} elapsed=${Date.now() - _wsOpenStart}ms\n`);
740
+ if (process.env.MIXDOG_DEBUG_AGENT) {
741
+ process.stderr.write(`[agent-trace] ws-open-fail kind=acquire_timeout timeoutMs=${WS_ACQUIRE_TIMEOUT_MS} elapsed=${Date.now() - _wsOpenStart}ms\n`);
551
742
  }
552
743
  try { socket.terminate(); } catch {}
553
744
  settle(false, Object.assign(
@@ -564,14 +755,14 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }
564
755
  } catch {}
565
756
  });
566
757
  socket.once('open', () => {
567
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
568
- process.stderr.write(`[bridge-trace] ws-open-ok elapsed=${Date.now() - _wsOpenStart}ms\n`);
758
+ if (process.env.MIXDOG_DEBUG_AGENT) {
759
+ process.stderr.write(`[agent-trace] ws-open-ok elapsed=${Date.now() - _wsOpenStart}ms\n`);
569
760
  }
570
761
  settle(true, { socket, turnState: capturedHeaders.turnState });
571
762
  });
572
763
  socket.once('error', (err) => {
573
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
574
- process.stderr.write(`[bridge-trace] ws-open-fail kind=error msg=${String(err?.message || err).slice(0, 120)} elapsed=${Date.now() - _wsOpenStart}ms\n`);
764
+ if (process.env.MIXDOG_DEBUG_AGENT) {
765
+ process.stderr.write(`[agent-trace] ws-open-fail kind=error msg=${String(err?.message || err).slice(0, 120)} elapsed=${Date.now() - _wsOpenStart}ms\n`);
575
766
  }
576
767
  try { socket.terminate(); } catch {}
577
768
  settle(false, err instanceof Error ? err : Object.assign(new Error(errText(err) || 'openai-oauth WS error'), { wsErrorEvent: true, original: err }));
@@ -594,8 +785,8 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }
594
785
  let body = '';
595
786
  res.on('data', c => { if (body.length < 2048) body += c.toString('utf-8'); });
596
787
  res.on('end', () => {
597
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
598
- process.stderr.write(`[bridge-trace] ws-open-fail kind=http status=${status} body=${body.slice(0, 120)} elapsed=${Date.now() - _wsOpenStart}ms\n`);
788
+ if (process.env.MIXDOG_DEBUG_AGENT) {
789
+ process.stderr.write(`[agent-trace] ws-open-fail kind=http status=${status} body=${body.slice(0, 120)} elapsed=${Date.now() - _wsOpenStart}ms\n`);
599
790
  }
600
791
  try { socket.terminate(); } catch {}
601
792
  settle(false, Object.assign(new Error(`${_wsErrLabel(auth?.type === 'xai' ? 'xai' : auth?.type === 'openai-direct' ? 'openai-direct' : 'openai-oauth')} handshake ${status}: ${body.slice(0, 200)}`), { httpStatus: status, httpBody: body }));
@@ -616,12 +807,12 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey }
616
807
 
617
808
  async function acquireWebSocket({ auth, poolKey, cacheKey, forceFresh, externalSignal }) {
618
809
  const _acqStart = Date.now();
619
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
620
- process.stderr.write(`[bridge-trace] acquire-start poolKey=${poolKey} cacheKey=${cacheKey} forceFresh=${forceFresh} externalAborted=${!!externalSignal?.aborted} ts=${_acqStart}\n`);
810
+ if (process.env.MIXDOG_DEBUG_AGENT) {
811
+ process.stderr.write(`[agent-trace] acquire-start poolKey=${poolKey} cacheKey=${cacheKey} forceFresh=${forceFresh} externalAborted=${!!externalSignal?.aborted} ts=${_acqStart}\n`);
621
812
  }
622
813
  if (externalSignal?.aborted) {
623
814
  const reason = externalSignal.reason;
624
- throw reason instanceof Error ? reason : new Error('Codex WS acquire aborted');
815
+ throw reason instanceof Error ? reason : new Error('OpenAI OAuth WS acquire aborted');
625
816
  }
626
817
  if (poolKey && !forceFresh) {
627
818
  const arr = _wsPool.get(poolKey) || [];
@@ -645,15 +836,15 @@ async function acquireWebSocket({ auth, poolKey, cacheKey, forceFresh, externalS
645
836
  if (idle.lastInputPrefixHash === undefined) idle.lastInputPrefixHash = null;
646
837
  if (idle.lastRequestInput === undefined) idle.lastRequestInput = null;
647
838
  if (idle.lastResponseItems === undefined) idle.lastResponseItems = null;
648
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
649
- process.stderr.write(`[bridge-trace] acquire-reuse poolKey=${poolKey} openSockets=${arr.length} elapsed=${Date.now() - _acqStart}ms\n`);
839
+ if (process.env.MIXDOG_DEBUG_AGENT) {
840
+ process.stderr.write(`[agent-trace] acquire-reuse poolKey=${poolKey} openSockets=${arr.length} elapsed=${Date.now() - _acqStart}ms\n`);
650
841
  }
651
842
  return { entry: idle, reused: true };
652
843
  }
653
844
  // All entries busy and bucket at cap: fall through to ephemeral socket.
654
845
  if (arr.length >= MAX_POOLED_SOCKETS_PER_KEY) {
655
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
656
- process.stderr.write(`[bridge-trace] acquire-ephemeral cacheKey=${cacheKey} reason=cap elapsed=${Date.now() - _acqStart}ms\n`);
846
+ if (process.env.MIXDOG_DEBUG_AGENT) {
847
+ process.stderr.write(`[agent-trace] acquire-ephemeral cacheKey=${cacheKey} reason=cap elapsed=${Date.now() - _acqStart}ms\n`);
657
848
  }
658
849
  const ephSessionToken = _mintSessionToken(cacheKey, auth);
659
850
  const { socket, turnState } = await _openSocket({ auth, sessionToken: ephSessionToken, turnState: null, externalSignal, cacheKey });
@@ -683,13 +874,13 @@ async function acquireWebSocket({ auth, poolKey, cacheKey, forceFresh, externalS
683
874
  return { entry, reused: false };
684
875
  }
685
876
  }
686
- // Parallel sockets must not inherit sibling turnState or the Codex server
877
+ // Parallel sockets must not inherit sibling turnState or the openai-oauth server
687
878
  // treats the new request as a continuation of another in-flight turn and
688
879
  // returns "No tool output found for function call …". turnState only
689
880
  // propagates within a single entry across its own iterations.
690
881
  const sessionToken = _mintSessionToken(cacheKey, auth);
691
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
692
- process.stderr.write(`[bridge-trace] acquire-new tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} elapsed=${Date.now() - _acqStart}ms\n`);
882
+ if (process.env.MIXDOG_DEBUG_AGENT) {
883
+ process.stderr.write(`[agent-trace] acquire-new tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} elapsed=${Date.now() - _acqStart}ms\n`);
693
884
  }
694
885
  const { socket, turnState } = await _openSocket({ auth, sessionToken, turnState: null, externalSignal, cacheKey });
695
886
  const entry = {
@@ -780,16 +971,41 @@ function _contentPartsEqual(a, b) {
780
971
  return _stableStringify(aa) === _stableStringify(bb);
781
972
  }
782
973
 
783
- function _logicalResponseItemMatch(inputItem, responseItem) {
974
+ export function _logicalResponseItemMatch(inputItem, responseItem) {
784
975
  if (!inputItem || !responseItem) return false;
785
976
  const inputType = inputItem.type || (inputItem.role === 'assistant' ? 'message' : '');
786
977
  const responseType = responseItem.type || '';
787
978
  if (responseType === 'function_call') {
788
- return inputType === 'function_call'
789
- && String(inputItem.call_id || '') === String(responseItem.call_id || '')
790
- && String(inputItem.name || '') === String(responseItem.name || '')
979
+ if (inputType !== 'function_call') return false;
980
+ const inputCallId = String(inputItem.call_id || '');
981
+ const responseCallId = String(responseItem.call_id || '');
982
+ const inputName = String(inputItem.name || '');
983
+ const responseName = String(responseItem.name || '');
984
+ if (inputCallId && responseCallId) {
985
+ // call_id is the server-side anchor. The replayed history may carry
986
+ // locally compacted arguments, but previous_response_id already
987
+ // points at the canonical output item.
988
+ return inputCallId === responseCallId && inputName === responseName;
989
+ }
990
+ return inputName === responseName
791
991
  && _normalizeArguments(inputItem.arguments) === _normalizeArguments(responseItem.arguments);
792
992
  }
993
+ if (responseType === 'tool_search_call') {
994
+ if (inputType !== 'tool_search_call') return false;
995
+ const inputCallId = String(inputItem.call_id || '');
996
+ const responseCallId = String(responseItem.call_id || '');
997
+ if (inputCallId && responseCallId) return inputCallId === responseCallId;
998
+ return _normalizeArguments(inputItem.arguments) === _normalizeArguments(responseItem.arguments);
999
+ }
1000
+ if (responseType === 'custom_tool_call') {
1001
+ if (inputType !== 'custom_tool_call') return false;
1002
+ const inputCallId = String(inputItem.call_id || '');
1003
+ const responseCallId = String(responseItem.call_id || '');
1004
+ const inputName = String(inputItem.name || '');
1005
+ const responseName = String(responseItem.name || '');
1006
+ if (inputCallId && responseCallId) return inputCallId === responseCallId && inputName === responseName;
1007
+ return inputName === responseName && String(inputItem.input || '') === String(responseItem.input || '');
1008
+ }
793
1009
  if (responseType === 'message') {
794
1010
  const inputRole = inputItem.role || (inputType === 'message' ? 'assistant' : '');
795
1011
  const responseRole = responseItem.role || 'assistant';
@@ -838,6 +1054,7 @@ function _isReplayLikeHead(item, responseItem) {
838
1054
  const responseType = responseItem.type || '';
839
1055
  if (responseType === 'message') return inputType === 'message';
840
1056
  if (responseType === 'function_call') return inputType === 'function_call';
1057
+ if (responseType === 'tool_search_call') return inputType === 'tool_search_call';
841
1058
  return inputType === responseType;
842
1059
  }
843
1060
 
@@ -969,7 +1186,25 @@ function _httpStatusFromWsClose(code, reason) {
969
1186
  function _wsErrLabel(p) {
970
1187
  if (p === 'xai') return 'xAI WS';
971
1188
  if (p === 'openai-direct' || p === 'openai') return 'OpenAI WS';
972
- return 'Codex WS';
1189
+ return 'OpenAI OAuth WS';
1190
+ }
1191
+ // tool_search_call.arguments parse. Module-scope (exported) for direct test
1192
+ // coverage. Native convergence (openai-oauth / anthropic-oauth / opencode): same policy
1193
+ // as the function_call_arguments.done path and openai-oauth _parseJsonObject —
1194
+ // object passes through; null/non-string/empty/whitespace → {} (no args); a
1195
+ // non-empty string that fails JSON.parse is deterministic bad JSON, surfaced
1196
+ // as an invalid-args MARKER (not silently swallowed to {}) so the dispatch
1197
+ // loop returns an is_error tool_result and the model self-corrects in the same
1198
+ // turn.
1199
+ export function parseToolSearchArgs(value) {
1200
+ if (value && typeof value === 'object') return value;
1201
+ if (typeof value !== 'string' || !value.trim()) return {};
1202
+ try {
1203
+ const parsed = JSON.parse(value);
1204
+ return parsed && typeof parsed === 'object' ? parsed : {};
1205
+ } catch (err) {
1206
+ return makeInvalidToolArgsMarker(value, err instanceof Error ? err.message : String(err));
1207
+ }
973
1208
  }
974
1209
  export async function _streamResponse({
975
1210
  entry,
@@ -996,7 +1231,6 @@ export async function _streamResponse({
996
1231
  const toolCalls = [];
997
1232
  const webSearchCalls = [];
998
1233
  const webSearchCallKeys = new Set();
999
- const compactionItems = [];
1000
1234
  const responseItemsAdded = [];
1001
1235
  const responseItemKeys = new Set();
1002
1236
  const citations = [];
@@ -1006,7 +1240,7 @@ export async function _streamResponse({
1006
1240
  // from response.completed.response.output). The request still includes
1007
1241
  // `reasoning.encrypted_content` so the server keeps emitting the blobs,
1008
1242
  // but explicit input-side replay is INTENTIONALLY OMITTED in
1009
- // convertMessagesToResponsesInput (openai-oauth.mjs:233-238) — Codex
1243
+ // convertMessagesToResponsesInput (openai-oauth.mjs:233-238) — openai-oauth
1010
1244
  // rejects the same `rs_*` id twice in one handshake session_id with a
1011
1245
  // "Duplicate item" error. Server-side conversation state already carries
1012
1246
  // the prefix forward across the WS_IDLE_MS window. The collected
@@ -1085,10 +1319,26 @@ export async function _streamResponse({
1085
1319
  for (const url of action.urls) pushCitation({ url, title: action.query || '' });
1086
1320
  }
1087
1321
  };
1088
- const pushCompactionItem = (item) => {
1089
- if (!item || !['compaction', 'compaction_summary', 'context_compaction'].includes(item.type)) return;
1090
- if (!item.encrypted_content) return;
1091
- compactionItems.push(item);
1322
+ const pushCustomToolCall = (item) => {
1323
+ const call = customToolCallFromResponseItem(item);
1324
+ if (!call || toolCalls.some((existing) => existing.id === call.id)) return;
1325
+ toolCalls.push(call);
1326
+ midState.emittedToolCall = true;
1327
+ try { onToolCall?.(call); } catch {}
1328
+ };
1329
+ const pushToolSearchCall = (item) => {
1330
+ if (!item || item.type !== 'tool_search_call') return;
1331
+ const callId = item.call_id || item.id || '';
1332
+ if (!callId || toolCalls.some((call) => call.id === callId)) return;
1333
+ const call = {
1334
+ id: callId,
1335
+ name: 'tool_search',
1336
+ arguments: parseToolSearchArgs(item.arguments),
1337
+ nativeType: 'tool_search_call',
1338
+ };
1339
+ toolCalls.push(call);
1340
+ midState.emittedToolCall = true;
1341
+ try { onToolCall?.(call); } catch {}
1092
1342
  };
1093
1343
  const logReasoningDeltaSuppression = () => {
1094
1344
  if (!logSuppressedReasoningDeltas) return;
@@ -1128,13 +1378,13 @@ export async function _streamResponse({
1128
1378
  // window is intentionally short (~25s). Once response.created (or
1129
1379
  // any other meaningful event) arrives, the timer is cancelled and
1130
1380
  // the longer inter-chunk inactivity watchdog takes over — silent
1131
- // gaps mid-reasoning (Codex spending 50s+ producing reasoning
1381
+ // gaps mid-reasoning (openai-oauth spending 50s+ producing reasoning
1132
1382
  // tokens) are normal and should not abort the turn.
1133
1383
  const armPreStreamWatchdog = () => {
1134
1384
  if (idleTimer) clearTimeout(idleTimer);
1135
1385
  idleTimer = setTimeout(() => {
1136
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
1137
- process.stderr.write(`[bridge-trace] ws-timeout kind=first-byte afterMs=${preResponseCreatedMs}\n`);
1386
+ if (process.env.MIXDOG_DEBUG_AGENT) {
1387
+ process.stderr.write(`[agent-trace] ws-timeout kind=first-byte afterMs=${preResponseCreatedMs}\n`);
1138
1388
  }
1139
1389
  traceWsTimeout('first_byte_timeout', preResponseCreatedMs);
1140
1390
  const err = new Error(`WS stream: no first server event within ${preResponseCreatedMs}ms`);
@@ -1178,7 +1428,7 @@ export async function _streamResponse({
1178
1428
  saw_response_created: midState.sawResponseCreated === true,
1179
1429
  first_meaningful_seen: firstMeaningfulSeen === true,
1180
1430
  };
1181
- appendBridgeTrace({
1431
+ appendAgentTrace({
1182
1432
  sessionId: midState.sessionId || null,
1183
1433
  iteration: Number.isFinite(iteration) ? iteration : null,
1184
1434
  kind: 'ws_timeout',
@@ -1203,8 +1453,8 @@ export async function _streamResponse({
1203
1453
  if (firstMeaningfulSeen || firstMeaningfulMs <= 0) return;
1204
1454
  clearFirstMeaningfulWatchdog();
1205
1455
  firstMeaningfulTimer = setTimeout(() => {
1206
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
1207
- process.stderr.write(`[bridge-trace] ws-timeout kind=first-meaningful afterMs=${firstMeaningfulMs}\n`);
1456
+ if (process.env.MIXDOG_DEBUG_AGENT) {
1457
+ process.stderr.write(`[agent-trace] ws-timeout kind=first-meaningful afterMs=${firstMeaningfulMs}\n`);
1208
1458
  }
1209
1459
  traceWsTimeout('first_meaningful_timeout', firstMeaningfulMs);
1210
1460
  const err = new Error(`WS stream: no meaningful output within ${firstMeaningfulMs}ms after response.created`);
@@ -1219,8 +1469,8 @@ export async function _streamResponse({
1219
1469
  const resetInterChunk = () => {
1220
1470
  if (interChunkTimer) clearTimeout(interChunkTimer);
1221
1471
  interChunkTimer = setTimeout(() => {
1222
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
1223
- process.stderr.write(`[bridge-trace] ws-timeout kind=inter-chunk afterMs=${interChunkMs}\n`);
1472
+ if (process.env.MIXDOG_DEBUG_AGENT) {
1473
+ process.stderr.write(`[agent-trace] ws-timeout kind=inter-chunk afterMs=${interChunkMs}\n`);
1224
1474
  }
1225
1475
  traceWsTimeout('inter_chunk_timeout', interChunkMs);
1226
1476
  terminalError = new Error(`WS stream: inter-chunk inactivity for ${interChunkMs}ms`);
@@ -1237,7 +1487,7 @@ export async function _streamResponse({
1237
1487
  };
1238
1488
  // Called on every event that carries real output tokens or tool
1239
1489
  // progress. `response.created` is only an ACK and must not count here:
1240
- // a wedged Codex stream can ACK immediately and then never produce
1490
+ // a wedged openai-oauth stream can ACK immediately and then never produce
1241
1491
  // text/reasoning/tool deltas, holding the prompt-cache lane for the
1242
1492
  // full inter-chunk window.
1243
1493
  const onMeaningfulOutput = () => {
@@ -1263,19 +1513,12 @@ export async function _streamResponse({
1263
1513
  const finish = () => {
1264
1514
  logReasoningDeltaSuppression();
1265
1515
  cleanup();
1266
- if (!terminalError && midState.expectCompaction === true && compactionItems.length !== 1) {
1267
- terminalError = new Error(
1268
- `${errLabel} remote compaction expected exactly one compaction output item, got ${compactionItems.length}`,
1269
- );
1270
- }
1271
1516
  if (terminalError) { reject(terminalError); return; }
1272
1517
  resolve({
1273
1518
  content,
1274
1519
  model,
1275
1520
  reasoningItems: reasoningItems.length ? reasoningItems : undefined,
1276
1521
  responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
1277
- compactionItem: compactionItems.length === 1 ? compactionItems[0] : undefined,
1278
- compactionItems: compactionItems.length ? compactionItems : undefined,
1279
1522
  toolCalls: toolCalls.length ? toolCalls : undefined,
1280
1523
  citations: citations.length ? citations : undefined,
1281
1524
  webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
@@ -1290,7 +1533,7 @@ export async function _streamResponse({
1290
1533
  messageHandler = (data) => {
1291
1534
  resetIdle();
1292
1535
  // Do NOT call onStreamDelta for every frame — metadata/keepalive frames
1293
- // must not reset bridge-stall-watchdog's lastStreamDeltaAt. Only
1536
+ // must not reset the agent stall watchdog's lastStreamDeltaAt. Only
1294
1537
  // meaningful output (text delta / tool call) updates that timestamp.
1295
1538
  const text = typeof data === 'string' ? data : data.toString('utf-8');
1296
1539
  const event = _parseEvent(text);
@@ -1321,8 +1564,8 @@ export async function _streamResponse({
1321
1564
  try {
1322
1565
  if (!_firstDeltaEmitted) {
1323
1566
  _firstDeltaEmitted = true;
1324
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
1325
- process.stderr.write(`[bridge-trace] ws-first-delta sinceStreaming=${Date.now() - _streamingStart}ms\n`);
1567
+ if (process.env.MIXDOG_DEBUG_AGENT) {
1568
+ process.stderr.write(`[agent-trace] ws-first-delta sinceStreaming=${Date.now() - _streamingStart}ms\n`);
1326
1569
  }
1327
1570
  }
1328
1571
  onStreamDelta?.();
@@ -1370,11 +1613,30 @@ export async function _streamResponse({
1370
1613
  try { onStreamDelta?.(); } catch {}
1371
1614
  onMeaningfulOutput();
1372
1615
  break;
1616
+ case 'response.custom_tool_call_input.delta':
1617
+ try { onStreamDelta?.(); } catch {}
1618
+ onMeaningfulOutput();
1619
+ break;
1373
1620
  case 'response.function_call_arguments.done': {
1374
1621
  const itemId = event.item_id || '';
1375
1622
  const pending = pendingCalls.get(itemId);
1623
+ // function_call_arguments.done is a completion signal:
1624
+ // empty/whitespace → no args ({}); a non-empty string that
1625
+ // fails JSON.parse is deterministic bad JSON. Native
1626
+ // convergence: surface an invalid-args MARKER (not silent
1627
+ // {}) so the dispatch loop returns an is_error tool_result
1628
+ // and the model re-issues valid JSON in the same turn.
1376
1629
  let args = {};
1377
- try { args = JSON.parse(event.arguments || '{}'); } catch {}
1630
+ {
1631
+ const _argText = typeof event.arguments === 'string' ? event.arguments : '';
1632
+ if (_argText.trim() !== '') {
1633
+ try {
1634
+ args = JSON.parse(_argText);
1635
+ } catch (err) {
1636
+ args = makeInvalidToolArgsMarker(_argText, err instanceof Error ? err.message : String(err));
1637
+ }
1638
+ }
1639
+ }
1378
1640
  enrichFunctionCallResponseItem({
1379
1641
  itemId,
1380
1642
  callId: pending?.callId || event.call_id || '',
@@ -1413,12 +1675,16 @@ export async function _streamResponse({
1413
1675
  // function_call / output_text already captured via their
1414
1676
  // dedicated streaming events. The one shape we still need
1415
1677
  // here is `reasoning` — carries encrypted_content that
1416
- // must be replayed on the next input to keep the Codex
1678
+ // must be replayed on the next input to keep the openai-oauth
1417
1679
  // server-side prompt cache prefix warm.
1418
1680
  if (event.item?.type === 'reasoning') pushReasoningItem(event.item);
1419
1681
  if (event.item?.type === 'web_search_call') pushWebSearchCall(event.item);
1420
- if (['compaction', 'compaction_summary', 'context_compaction'].includes(event.item?.type)) {
1421
- pushCompactionItem(event.item);
1682
+ if (event.item?.type === 'tool_search_call') {
1683
+ pushToolSearchCall(event.item);
1684
+ onMeaningfulOutput();
1685
+ }
1686
+ if (event.item?.type === 'custom_tool_call') {
1687
+ pushCustomToolCall(event.item);
1422
1688
  onMeaningfulOutput();
1423
1689
  }
1424
1690
  break;
@@ -1434,7 +1700,7 @@ export async function _streamResponse({
1434
1700
  inputTokens: u.input_tokens || 0,
1435
1701
  outputTokens: u.output_tokens || 0,
1436
1702
  cachedTokens: extractCachedTokens(u),
1437
- // OpenAI Codex reports input_tokens as the total
1703
+ // openai-oauth reports input_tokens as the total
1438
1704
  // prompt volume (cached portion is a subset, not
1439
1705
  // additive). Alias into the cross-provider
1440
1706
  // `promptTokens` field so downstream loggers have
@@ -1462,9 +1728,8 @@ export async function _streamResponse({
1462
1728
  }
1463
1729
  }
1464
1730
  if (item.type === 'web_search_call') pushWebSearchCall(item);
1465
- if (['compaction', 'compaction_summary', 'context_compaction'].includes(item.type)) {
1466
- pushCompactionItem(item);
1467
- }
1731
+ if (item.type === 'tool_search_call') pushToolSearchCall(item);
1732
+ if (item.type === 'custom_tool_call') pushCustomToolCall(item);
1468
1733
  // Salvage path: some streams emit reasoning only
1469
1734
  // inside the final response.completed.output
1470
1735
  // bundle (no per-item .done event). Dedup by id.
@@ -1514,7 +1779,7 @@ export async function _streamResponse({
1514
1779
  break;
1515
1780
  }
1516
1781
  case 'response.done': {
1517
- // response.done is the terminal frame for some Codex
1782
+ // response.done is the terminal frame for some openai-oauth
1518
1783
  // streams that never emit a separate response.completed.
1519
1784
  // Route through the same completed/failed/incomplete
1520
1785
  // normalization based on event.response.status so a
@@ -1638,7 +1903,7 @@ export async function _streamResponse({
1638
1903
  const r = reason?.toString?.('utf-8') || '';
1639
1904
  const httpStatus = _httpStatusFromWsClose(code, r);
1640
1905
  terminalError = Object.assign(
1641
- new Error(`Codex WS closed before response.completed (code=${code}${r ? `, reason=${r}` : ''})`),
1906
+ new Error(`OpenAI OAuth WS closed before response.completed (code=${code}${r ? `, reason=${r}` : ''})`),
1642
1907
  { wsCloseCode: code, wsCloseReason: r, ...(httpStatus ? { httpStatus } : {}) },
1643
1908
  );
1644
1909
  } else if (terminalError && !terminalError.wsCloseCode) {
@@ -1674,15 +1939,15 @@ export async function _streamResponse({
1674
1939
  abortHandler = () => {
1675
1940
  if (done) return;
1676
1941
  const reason = externalSignal.reason;
1677
- terminalError = reason instanceof Error ? reason : new Error('Codex WS aborted by session close');
1942
+ terminalError = reason instanceof Error ? reason : new Error('OpenAI OAuth WS aborted by session close');
1678
1943
  // Tag: was this a user/caller abort, or a watchdog abort?
1679
1944
  // Mid-stream retry must skip user aborts but may retry watchdog
1680
1945
  // aborts. The caller-owned AbortController surfaces through
1681
- // externalSignal; bridge-stall-watchdog signals via a reason
1682
- // object whose name === 'BridgeStallAbortError'. stream-watchdog
1946
+ // externalSignal; the agent stall watchdog signals via a reason
1947
+ // object whose name === 'AgentStallAbortError'. stream-watchdog
1683
1948
  // uses StreamStalledAbortError. Anything else → treat as user.
1684
1949
  const reasonName = reason?.name || '';
1685
- if (reasonName === 'BridgeStallAbortError'
1950
+ if (reasonName === 'AgentStallAbortError'
1686
1951
  || reasonName === 'StreamStalledAbortError') {
1687
1952
  midState.watchdogAbort = reasonName;
1688
1953
  } else {
@@ -1698,10 +1963,10 @@ export async function _streamResponse({
1698
1963
  socket.on('close', closeHandler);
1699
1964
  socket.on('error', errorHandler);
1700
1965
  armPreStreamWatchdog();
1701
- // Periodic client-side WS ping while the stream is active. Codex's
1966
+ // Periodic client-side WS ping while the stream is active. The server's
1702
1967
  // server closes with 1011 "keepalive ping timeout" when it thinks the
1703
1968
  // peer is silent during long reasoning windows where no data frames
1704
- // flow. Sending a ping every 18s from our side keeps the socket warm.
1969
+ // flow. Sending a ping every 10s from our side keeps the socket warm.
1705
1970
  // The interval is unref'd so it never holds the event loop open, and
1706
1971
  // cleanup() clears it on every terminal path (completed / close /
1707
1972
  // error / abort / mid-stream retry teardown).
@@ -1710,7 +1975,7 @@ export async function _streamResponse({
1710
1975
  if (socket.readyState !== WebSocket.OPEN) return;
1711
1976
  socket.ping();
1712
1977
  } catch {}
1713
- }, 18_000);
1978
+ }, 10_000);
1714
1979
  try { keepaliveTimer.unref?.(); } catch {}
1715
1980
  });
1716
1981
  }
@@ -1771,7 +2036,7 @@ export function _classifyHandshakeError(err) {
1771
2036
  * after completion would replay a finished turn.
1772
2037
  *
1773
2038
  * Retry buckets:
1774
- * 'bridge_stall' BridgeStallAbortError from bridge-stall-watchdog
2039
+ * 'agent_stall' AgentStallAbortError from agent stall watchdog
1775
2040
  * 'stream_stalled' — StreamStalledAbortError from stream-watchdog
1776
2041
  * 'ws_1006' — abnormal close (connection lost)
1777
2042
  * 'ws_1011' — server unexpected condition
@@ -1811,7 +2076,7 @@ export function _classifyMidstreamError(err, state) {
1811
2076
  // leaves the caller with an orphaned tool_use that the next turn cannot
1812
2077
  // pair to a tool_result, which the provider rejects with a hard 400.
1813
2078
  // The duplicate-side-effect risk is preferable to deterministic worker
1814
- // death, especially for detached bridges that re-dispatch idempotently.
2079
+ // death, especially for detached agents that re-dispatch idempotently.
1815
2080
  if (state.emittedToolCall) {
1816
2081
  const _cc = Number(err?.wsCloseCode || state.wsCloseCode || 0);
1817
2082
  if (!(_cc === 1000 && state.sawResponseCreated && !state.sawCompleted)) return null;
@@ -1867,13 +2132,13 @@ export function _classifyMidstreamError(err, state) {
1867
2132
  }
1868
2133
 
1869
2134
  const name = err?.name || '';
1870
- if (name === 'BridgeStallAbortError') return _allowMidstreamRetry('bridge_stall', attemptIndex);
2135
+ if (name === 'AgentStallAbortError') return _allowMidstreamRetry('agent_stall', attemptIndex);
1871
2136
  if (name === 'StreamStalledAbortError') return _allowMidstreamRetry('stream_stalled', attemptIndex);
1872
2137
 
1873
2138
  // Watchdog abort surfaced via externalSignal handler → err is the reason
1874
2139
  // itself. state.watchdogAbort captures the class name when the error
1875
2140
  // shape was preserved but the name was stripped by some wrapper.
1876
- if (state.watchdogAbort === 'BridgeStallAbortError') return _allowMidstreamRetry('bridge_stall', attemptIndex);
2141
+ if (state.watchdogAbort === 'AgentStallAbortError') return _allowMidstreamRetry('agent_stall', attemptIndex);
1877
2142
  if (state.watchdogAbort === 'StreamStalledAbortError') return _allowMidstreamRetry('stream_stalled', attemptIndex);
1878
2143
 
1879
2144
  // WS close codes: prefer the decorated property, fall back to state.
@@ -1923,7 +2188,8 @@ function _allowMidstreamRetry(classifier, attemptIndex) {
1923
2188
  }
1924
2189
 
1925
2190
  function _midstreamBackoffFor(retryNumber) {
1926
- return MIDSTREAM_BACKOFF_MS[Math.min(Math.max(retryNumber, 1), MIDSTREAM_BACKOFF_MS.length) - 1];
2191
+ const raw = MIDSTREAM_BACKOFF_MS[Math.min(Math.max(retryNumber, 1), MIDSTREAM_BACKOFF_MS.length) - 1];
2192
+ return jitterDelayMs(raw);
1927
2193
  }
1928
2194
 
1929
2195
  function _backoffFor(attempt) {
@@ -1948,7 +2214,7 @@ async function _sleepWithAbort(ms, externalSignal, sleepFn = _defaultSleep) {
1948
2214
  const onAbort = () => {
1949
2215
  clearTimeout(t);
1950
2216
  const reason = externalSignal.reason;
1951
- reject(reason instanceof Error ? reason : new Error('Codex WS retry backoff aborted'));
2217
+ reject(reason instanceof Error ? reason : new Error('OpenAI OAuth WS retry backoff aborted'));
1952
2218
  };
1953
2219
  if (externalSignal.aborted) { onAbort(); return; }
1954
2220
  externalSignal.addEventListener('abort', onAbort, { once: true });
@@ -1980,12 +2246,12 @@ export async function _acquireWithRetry({
1980
2246
  for (let attempt = 1; attempt <= HANDSHAKE_MAX_ATTEMPTS; attempt++) {
1981
2247
  if (externalSignal?.aborted) {
1982
2248
  const reason = externalSignal.reason;
1983
- throw reason instanceof Error ? reason : new Error('Codex WS acquire aborted');
2249
+ throw reason instanceof Error ? reason : new Error('OpenAI OAuth WS acquire aborted');
1984
2250
  }
1985
2251
  try {
1986
2252
  if (attempt > 1) {
1987
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
1988
- process.stderr.write(`[bridge-trace] ws-handshake-attempt n=${attempt}\n`);
2253
+ if (process.env.MIXDOG_DEBUG_AGENT) {
2254
+ process.stderr.write(`[agent-trace] ws-handshake-attempt n=${attempt}\n`);
1989
2255
  }
1990
2256
  }
1991
2257
  return await _acquire({ auth, poolKey, cacheKey, forceFresh, externalSignal });
@@ -2041,7 +2307,7 @@ export async function _acquireWithRetry({
2041
2307
  const onAbort = () => {
2042
2308
  clearTimeout(t);
2043
2309
  const reason = externalSignal.reason;
2044
- reject(reason instanceof Error ? reason : new Error('Codex WS acquire aborted'));
2310
+ reject(reason instanceof Error ? reason : new Error('OpenAI OAuth WS acquire aborted'));
2045
2311
  };
2046
2312
  if (externalSignal.aborted) { onAbort(); return; }
2047
2313
  externalSignal.addEventListener('abort', onAbort, { once: true });
@@ -2121,12 +2387,21 @@ export async function sendViaWebSocket({
2121
2387
  }
2122
2388
  return e;
2123
2389
  };
2390
+ // Tool-call relay invariant: same latch pattern as live text — once a tool
2391
+ // call was forwarded, surfaced errors must block HTTP fallback / reissue.
2392
+ let toolEmittedAcrossAttempts = false;
2393
+ const _stampTool = (e) => {
2394
+ if (toolEmittedAcrossAttempts && e) {
2395
+ try { e.emittedToolCall = true; e.unsafeToRetry = true; } catch {}
2396
+ }
2397
+ return e;
2398
+ };
2124
2399
  // Server-side xAI conversation anchor preserved across mid-stream
2125
2400
  // retries. xAI keys its conversation by previous_response_id alone
2126
2401
  // (sessionToken is null for xAI in _mintSessionToken); a forceFresh
2127
2402
  // socket on retry would otherwise drop prev_id and cold-start a new
2128
2403
  // server-side conversation, evicting every prefix the prior attempts
2129
- // warmed. Codex / openai-direct anchor by per-socket session_id, where
2404
+ // warmed. openai-oauth / openai-direct anchor by per-socket session_id, where
2130
2405
  // this carry-forward would not help and is therefore gated to xAI.
2131
2406
  let carryForwardCache = null;
2132
2407
  const emittedProgress = [];
@@ -2166,7 +2441,7 @@ export async function sendViaWebSocket({
2166
2441
  const classifiers = [...handshakeRetryClassifiers];
2167
2442
  if (classifier && !classifiers.includes(classifier)) classifiers.push(classifier);
2168
2443
  if (err?.httpStatus != null || classifier || handshakeRetries > 0 || classifiers.length > 0) {
2169
- traceBridgeFetch({
2444
+ traceAgentFetch({
2170
2445
  sessionId: poolKey,
2171
2446
  headersMs: Date.now() - handshakeStart,
2172
2447
  httpStatus: Number(err?.httpStatus || 0),
@@ -2182,9 +2457,9 @@ export async function sendViaWebSocket({
2182
2457
  // the caller's turn actually tripped on).
2183
2458
  if (attemptIndex > 0 && firstAttemptError) {
2184
2459
  try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
2185
- throw _stampLiveText(firstAttemptError);
2460
+ throw _stampTool(_stampLiveText(firstAttemptError));
2186
2461
  }
2187
- throw _stampLiveText(err);
2462
+ throw _stampTool(_stampLiveText(err));
2188
2463
  }
2189
2464
  const { entry, reused } = acquired;
2190
2465
  // Re-seed the retry attempt's fresh entry with the prior attempt's
@@ -2199,7 +2474,7 @@ export async function sendViaWebSocket({
2199
2474
  entry.lastRequestInput = carryForwardCache.lastRequestInput;
2200
2475
  entry.lastResponseItems = carryForwardCache.lastResponseItems;
2201
2476
  }
2202
- traceBridgeFetch({
2477
+ traceAgentFetch({
2203
2478
  sessionId: poolKey,
2204
2479
  headersMs: Date.now() - handshakeStart,
2205
2480
  httpStatus: reused ? 0 : 101,
@@ -2245,14 +2520,15 @@ export async function sendViaWebSocket({
2245
2520
  let strippedResponseItems = 0;
2246
2521
  let skippedResponseItems = 0;
2247
2522
  let result;
2248
- const streamTimeouts = sendOpts?.expectCompaction === true
2249
- ? {
2250
- firstMeaningfulMs: WS_INTER_CHUNK_MS,
2251
- interChunkMs: WS_INTER_CHUNK_MS,
2252
- }
2253
- : null;
2523
+ const streamTimeouts = null;
2254
2524
  try {
2255
2525
  if (warmupBody && typeof warmupBody === 'object' && attemptIndex === 0) {
2526
+ await promptCacheLane?.reserveRate?.({
2527
+ mode: 'full',
2528
+ frameInputItems: Array.isArray(warmupBody.input) ? warmupBody.input.length : null,
2529
+ deltaTokens: _estimateFrameTokens({ type: 'response.create', ...warmupBody }),
2530
+ hasPreviousResponseId: false,
2531
+ });
2256
2532
  const warmupFrame = { type: 'response.create', ...warmupBody };
2257
2533
  await _sendFrameFn(entry, warmupFrame);
2258
2534
  const warmupStart = Date.now();
@@ -2306,7 +2582,7 @@ export async function sendViaWebSocket({
2306
2582
  output_tokens: warmupResult.usage?.outputTokens || 0,
2307
2583
  prompt_tokens: warmupResult.usage?.promptTokens || 0,
2308
2584
  };
2309
- appendBridgeTrace({
2585
+ appendAgentTrace({
2310
2586
  sessionId: poolKey,
2311
2587
  iteration,
2312
2588
  kind: 'cache_warmup',
@@ -2329,6 +2605,12 @@ export async function sendViaWebSocket({
2329
2605
  strippedResponseItems = delta.strippedResponseItems || 0;
2330
2606
  skippedResponseItems = delta.skippedResponseItems || 0;
2331
2607
  deltaTokens = _estimateFrameTokens(frame);
2608
+ await promptCacheLane?.reserveRate?.({
2609
+ mode,
2610
+ frameInputItems: Array.isArray(frame.input) ? frame.input.length : null,
2611
+ deltaTokens,
2612
+ hasPreviousResponseId: typeof frame.previous_response_id === 'string' && frame.previous_response_id.length > 0,
2613
+ });
2332
2614
 
2333
2615
  // Re-check abort after acquire/warmup — narrow window where
2334
2616
  // externalSignal could fire between successful acquire and
@@ -2343,8 +2625,8 @@ export async function sendViaWebSocket({
2343
2625
  }
2344
2626
  await _sendFrameFn(entry, frame);
2345
2627
 
2346
- if (process.env.MIXDOG_DEBUG_BRIDGE) {
2347
- process.stderr.write(`[bridge-trace] ws-streaming-start sinceAcquire=${Date.now() - handshakeStart}ms\n`);
2628
+ if (process.env.MIXDOG_DEBUG_AGENT) {
2629
+ process.stderr.write(`[agent-trace] ws-streaming-start sinceAcquire=${Date.now() - handshakeStart}ms\n`);
2348
2630
  }
2349
2631
  try { onStageChange?.('streaming'); } catch {}
2350
2632
  result = await _streamFn({
@@ -2385,7 +2667,11 @@ export async function sendViaWebSocket({
2385
2667
  // error (firstAttemptError) must still carry the marker.
2386
2668
  liveTextEmittedAcrossAttempts = true;
2387
2669
  }
2670
+ if (midState.emittedToolCall) {
2671
+ toolEmittedAcrossAttempts = true;
2672
+ }
2388
2673
  _stampLiveText(err);
2674
+ _stampTool(err);
2389
2675
  const classifier = _classifyMidstreamError(err, midState);
2390
2676
  const retryLimit = classifier ? _midstreamRetryLimit(classifier) : 0;
2391
2677
  if (classifier && attemptIndex < retryLimit) {
@@ -2424,12 +2710,12 @@ export async function sendViaWebSocket({
2424
2710
  firstAttemptError.suppressed = list;
2425
2711
  }
2426
2712
  } catch {}
2427
- throw _stampLiveText(firstAttemptError);
2713
+ throw _stampTool(_stampLiveText(firstAttemptError));
2428
2714
  }
2429
- throw _stampLiveText(err);
2715
+ throw _stampTool(_stampLiveText(err));
2430
2716
  }
2431
2717
  const liveModel = result.model || useModel;
2432
- traceBridgeSse({
2718
+ traceAgentSse({
2433
2719
  sessionId: poolKey,
2434
2720
  sseParseMs: Date.now() - sseStart,
2435
2721
  provider: traceProvider,
@@ -2438,10 +2724,21 @@ export async function sendViaWebSocket({
2438
2724
  });
2439
2725
 
2440
2726
  const resultToolCallCount = Array.isArray(result.toolCalls) ? result.toolCalls.length : 0;
2441
- const keepResponseChain = !!result.responseId && !result.incompleteReason;
2727
+ // Keep the conversation chain whenever the server gave us a response id.
2728
+ // `incompleteReason` is ONLY ever set for max_output_tokens-class
2729
+ // truncation (every other incomplete status throws upstream), and in
2730
+ // that case the response IS valid and the server preserves its
2731
+ // response_id as a continuation anchor. Dropping the chain here forced
2732
+ // the NEXT turn to cold-start (no_anchor → full resend), which the
2733
+ // trace logs showed repeating 50-78x in long max-output sessions. If a
2734
+ // truncated turn's response items don't line up next turn,
2735
+ // _stripResponseItemsFromHead still falls back to a full send on its
2736
+ // own, so retaining the anchor cannot corrupt the cache — it only adds
2737
+ // a delta fast-path when the items DO match.
2738
+ const keepResponseChain = !!result.responseId;
2442
2739
  const keepSocket = true;
2443
2740
 
2444
- // Update cache state for the next iteration in this session. Codex
2741
+ // Update cache state for the next iteration in this session. openai-oauth
2445
2742
  // keeps the previous response anchor even when the model emitted tool
2446
2743
  // calls: the next request is previous input + server output items
2447
2744
  // + tool results, and _computeDelta strips the first two parts so the
@@ -2473,7 +2770,7 @@ export async function sendViaWebSocket({
2473
2770
 
2474
2771
  const requestedServiceTier = body?.service_tier || null;
2475
2772
  const responseServiceTier = result.serviceTier || result.usage?.raw?.service_tier || null;
2476
- traceBridgeUsage({
2773
+ traceAgentUsage({
2477
2774
  sessionId: poolKey,
2478
2775
  iteration,
2479
2776
  inputTokens: result.usage?.inputTokens || 0,
@@ -2489,6 +2786,9 @@ export async function sendViaWebSocket({
2489
2786
  });
2490
2787
  // Extra WS-specific observability: transport + per-iteration delta bytes.
2491
2788
  try {
2789
+ const transportCacheKeyHash = cacheKey
2790
+ ? createHash('sha256').update(String(cacheKey)).digest('hex').slice(0, 12)
2791
+ : null;
2492
2792
  const transportPayload = {
2493
2793
  provider: traceProvider,
2494
2794
  transport: 'websocket',
@@ -2504,15 +2804,18 @@ export async function sendViaWebSocket({
2504
2804
  handshake_retry_classifiers: handshakeRetryClassifiers,
2505
2805
  midstream_retries: attemptIndex,
2506
2806
  response_id: result.responseId || null,
2507
- cache_key_hash: cacheKey
2508
- ? createHash('sha256').update(String(cacheKey)).digest('hex').slice(0, 12)
2509
- : null,
2807
+ cache_key_hash: transportCacheKeyHash,
2510
2808
  cache_lane_enabled: promptCacheLane?.enabled === true,
2511
2809
  cache_lane_key_hash: promptCacheLane?.laneKeyHash || null,
2810
+ cache_lane_rate_policy: promptCacheLane?.ratePolicy || null,
2512
2811
  cache_lane_max_in_flight: Number.isFinite(Number(promptCacheLane?.maxInFlight)) ? Number(promptCacheLane.maxInFlight) : null,
2513
2812
  cache_lane_rate_limit_per_min: Number.isFinite(Number(promptCacheLane?.rateLimitPerMin)) ? Number(promptCacheLane.rateLimitPerMin) : null,
2813
+ cache_lane_rate_full_limit_per_min: Number.isFinite(Number(promptCacheLane?.rateFullLimitPerMin)) ? Number(promptCacheLane.rateFullLimitPerMin) : null,
2814
+ cache_lane_rate_delta_limit_per_min: Number.isFinite(Number(promptCacheLane?.rateDeltaLimitPerMin)) ? Number(promptCacheLane.rateDeltaLimitPerMin) : null,
2514
2815
  cache_lane_rate_wait_ms: Number.isFinite(Number(promptCacheLane?.rateWaitMs)) ? Number(promptCacheLane.rateWaitMs) : null,
2515
2816
  cache_lane_rate_window_count: Number.isFinite(Number(promptCacheLane?.rateWindowCount)) ? Number(promptCacheLane.rateWindowCount) : null,
2817
+ cache_lane_rate_released_for_wait: promptCacheLane?.rateReleasedForWait === true,
2818
+ cache_lane_rate_reacquire_wait_ms: Number.isFinite(Number(promptCacheLane?.rateReacquireWaitMs)) ? Number(promptCacheLane.rateReacquireWaitMs) : null,
2516
2819
  cache_lane_wait_ms: Number.isFinite(Number(promptCacheLane?.waitMs)) ? Number(promptCacheLane.waitMs) : null,
2517
2820
  cache_lane_queued: promptCacheLane?.queued === true,
2518
2821
  cache_lane_active: Number.isFinite(Number(promptCacheLane?.activeAfterAcquire)) ? Number(promptCacheLane.activeAfterAcquire) : null,
@@ -2531,13 +2834,40 @@ export async function sendViaWebSocket({
2531
2834
  keep_socket: keepSocket,
2532
2835
  keep_response_chain: keepResponseChain,
2533
2836
  };
2534
- appendBridgeTrace({
2837
+ appendAgentTrace({
2535
2838
  sessionId: poolKey,
2536
2839
  iteration,
2537
2840
  kind: 'transport',
2538
2841
  ...transportPayload,
2539
2842
  payload: transportPayload,
2540
2843
  });
2844
+ if (mode !== 'delta' || deltaReason) {
2845
+ appendAgentTrace({
2846
+ sessionId: poolKey,
2847
+ iteration,
2848
+ kind: 'cache_break',
2849
+ provider: traceProvider,
2850
+ model: liveModel,
2851
+ payload: {
2852
+ provider: traceProvider,
2853
+ model: liveModel,
2854
+ transport: 'websocket',
2855
+ ws_mode: mode,
2856
+ reason: mode === 'delta' ? deltaReason : (deltaReason || 'full_frame'),
2857
+ cache_key_hash: transportCacheKeyHash,
2858
+ cache_lane_key_hash: promptCacheLane?.laneKeyHash || null,
2859
+ request_has_previous_response_id: transportPayload.request_has_previous_response_id,
2860
+ chain_stripped_response_items: strippedResponseItems,
2861
+ chain_skipped_response_items: skippedResponseItems,
2862
+ chain_response_items: Array.isArray(result.responseItems) ? result.responseItems.length : 0,
2863
+ body_input_items: Array.isArray(requestBody.input) ? requestBody.input.length : null,
2864
+ frame_input_items: Array.isArray(frame.input) ? frame.input.length : null,
2865
+ frame_has_instructions: transportPayload.frame_has_instructions,
2866
+ keep_response_chain: keepResponseChain,
2867
+ tool_call_count: resultToolCallCount,
2868
+ },
2869
+ });
2870
+ }
2541
2871
  } catch {}
2542
2872
 
2543
2873
  releaseWebSocket({ entry, poolKey, keep: keepSocket });
@@ -2561,7 +2891,7 @@ export async function sendViaWebSocket({
2561
2891
  return out;
2562
2892
  }
2563
2893
  // Unreachable — the loop either returns or throws above.
2564
- throw _stampLiveText(firstAttemptError || new Error('sendViaWebSocket: unreachable'));
2894
+ throw _stampTool(_stampLiveText(firstAttemptError || new Error('sendViaWebSocket: unreachable')));
2565
2895
  });
2566
2896
  }
2567
2897