mixdog 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/package.json +9 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  6. package/scripts/background-task-meta-smoke.mjs +1 -1
  7. package/scripts/bench-run.mjs +262 -0
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/compact-smoke.mjs +12 -0
  10. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  11. package/scripts/explore-bench.mjs +124 -0
  12. package/scripts/hook-bus-test.mjs +191 -0
  13. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  14. package/scripts/internal-comms-bench.mjs +727 -0
  15. package/scripts/internal-comms-smoke.mjs +75 -0
  16. package/scripts/lead-workflow-smoke.mjs +4 -4
  17. package/scripts/live-worker-smoke.mjs +9 -9
  18. package/scripts/output-style-bench.mjs +285 -0
  19. package/scripts/output-style-smoke.mjs +13 -10
  20. package/scripts/patch-replay.mjs +90 -0
  21. package/scripts/path-suffix-test.mjs +57 -0
  22. package/scripts/provider-stream-stall-test.mjs +276 -0
  23. package/scripts/provider-toolcall-test.mjs +599 -1
  24. package/scripts/recall-bench.mjs +207 -0
  25. package/scripts/routing-corpus.mjs +281 -0
  26. package/scripts/session-bench.mjs +1526 -0
  27. package/scripts/session-diag.mjs +595 -0
  28. package/scripts/task-bench.mjs +207 -0
  29. package/scripts/tool-failures.mjs +6 -6
  30. package/scripts/tool-smoke.mjs +310 -67
  31. package/scripts/toolcall-args-test.mjs +81 -0
  32. package/src/agents/debugger/AGENT.md +4 -4
  33. package/src/agents/heavy-worker/AGENT.md +20 -9
  34. package/src/agents/reviewer/AGENT.md +4 -4
  35. package/src/agents/worker/AGENT.md +17 -9
  36. package/src/app.mjs +10 -6
  37. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  38. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  39. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  40. package/src/headless-role.mjs +14 -14
  41. package/src/help.mjs +1 -0
  42. package/src/lib/rules-builder.cjs +32 -54
  43. package/src/mixdog-session-runtime.mjs +1040 -2036
  44. package/src/output-styles/default.md +12 -7
  45. package/src/output-styles/minimal.md +25 -0
  46. package/src/output-styles/oneline.md +21 -0
  47. package/src/output-styles/simple.md +10 -9
  48. package/src/repl.mjs +17 -7
  49. package/src/rules/agent/00-common.md +7 -5
  50. package/src/rules/agent/30-explorer.md +8 -12
  51. package/src/rules/lead/01-general.md +3 -1
  52. package/src/rules/lead/lead-tool.md +13 -2
  53. package/src/rules/shared/01-tool.md +23 -12
  54. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  55. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  59. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  60. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
  62. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  63. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
  65. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  66. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
  70. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
  71. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
  72. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  73. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  74. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  75. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  76. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
  78. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
  79. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
  80. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  82. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
  83. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
  85. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  86. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  87. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  88. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  89. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  90. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  91. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  93. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  94. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  97. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  98. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  99. package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
  100. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  101. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  102. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  103. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  104. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  105. package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
  106. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  107. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  108. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  109. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  110. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  113. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  118. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  119. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  121. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
  123. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  124. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  125. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
  126. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  127. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  129. package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
  130. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  131. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  132. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  135. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  136. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  137. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  138. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  139. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  140. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  141. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  142. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  148. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  149. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  150. package/src/runtime/channels/backends/discord.mjs +99 -9
  151. package/src/runtime/channels/backends/telegram.mjs +501 -0
  152. package/src/runtime/channels/index.mjs +437 -1439
  153. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  154. package/src/runtime/channels/lib/config.mjs +54 -2
  155. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  156. package/src/runtime/channels/lib/format.mjs +4 -2
  157. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  158. package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
  159. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  160. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  161. package/src/runtime/channels/lib/telegram-format.mjs +280 -0
  162. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  164. package/src/runtime/channels/lib/webhook.mjs +59 -31
  165. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  166. package/src/runtime/channels/tool-defs.mjs +1 -1
  167. package/src/runtime/memory/index.mjs +465 -345
  168. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  169. package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
  170. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  171. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  172. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  173. package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
  174. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  175. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  176. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  177. package/src/runtime/memory/lib/memory.mjs +121 -4
  178. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  179. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  180. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  181. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  182. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  183. package/src/runtime/memory/tool-defs.mjs +10 -7
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/background-tasks.mjs +2 -3
  186. package/src/runtime/shared/buffered-appender.mjs +149 -0
  187. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  188. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  189. package/src/runtime/shared/config.mjs +9 -0
  190. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  191. package/src/runtime/shared/schedules-store.mjs +21 -19
  192. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  193. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  194. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  195. package/src/runtime/shared/tool-surface.mjs +98 -13
  196. package/src/runtime/shared/transcript-writer.mjs +156 -0
  197. package/src/runtime/shared/update-checker.mjs +214 -0
  198. package/src/session-runtime/config-helpers.mjs +209 -0
  199. package/src/session-runtime/effort.mjs +128 -0
  200. package/src/session-runtime/fs-utils.mjs +10 -0
  201. package/src/session-runtime/model-capabilities.mjs +130 -0
  202. package/src/session-runtime/output-styles.mjs +124 -0
  203. package/src/session-runtime/plugin-mcp.mjs +114 -0
  204. package/src/session-runtime/session-text.mjs +100 -0
  205. package/src/session-runtime/statusline-route.mjs +35 -0
  206. package/src/session-runtime/tool-catalog.mjs +720 -0
  207. package/src/session-runtime/workflow.mjs +358 -0
  208. package/src/standalone/agent-tool.mjs +302 -117
  209. package/src/standalone/channel-admin.mjs +133 -40
  210. package/src/standalone/channel-worker.mjs +10 -292
  211. package/src/standalone/explore-tool.mjs +12 -5
  212. package/src/standalone/hook-bus.mjs +165 -8
  213. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  214. package/src/standalone/opencode-go-login.mjs +121 -0
  215. package/src/standalone/provider-admin.mjs +25 -3
  216. package/src/standalone/usage-dashboard.mjs +1 -1
  217. package/src/tui/App.jsx +2883 -778
  218. package/src/tui/components/ConfirmBar.jsx +47 -0
  219. package/src/tui/components/ContextPanel.jsx +5 -3
  220. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  221. package/src/tui/components/Markdown.jsx +22 -98
  222. package/src/tui/components/Message.jsx +14 -35
  223. package/src/tui/components/Picker.jsx +87 -12
  224. package/src/tui/components/PromptInput.jsx +355 -22
  225. package/src/tui/components/QueuedCommands.jsx +1 -1
  226. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  227. package/src/tui/components/Spinner.jsx +7 -7
  228. package/src/tui/components/StatusLine.jsx +40 -21
  229. package/src/tui/components/TextEntryPanel.jsx +51 -7
  230. package/src/tui/components/ToolExecution.jsx +183 -101
  231. package/src/tui/components/TurnDone.jsx +4 -4
  232. package/src/tui/components/UsagePanel.jsx +1 -1
  233. package/src/tui/components/tool-output-format.mjs +161 -23
  234. package/src/tui/components/tool-output-format.test.mjs +87 -0
  235. package/src/tui/display-width.mjs +69 -0
  236. package/src/tui/display-width.test.mjs +35 -0
  237. package/src/tui/dist/index.mjs +6731 -2333
  238. package/src/tui/engine/agent-envelope.mjs +296 -0
  239. package/src/tui/engine/boot-profile.mjs +21 -0
  240. package/src/tui/engine/labels.mjs +67 -0
  241. package/src/tui/engine/notice-text.mjs +112 -0
  242. package/src/tui/engine/queue-helpers.mjs +161 -0
  243. package/src/tui/engine/session-stats.mjs +46 -0
  244. package/src/tui/engine/tool-call-fields.mjs +23 -0
  245. package/src/tui/engine/tool-result-text.mjs +126 -0
  246. package/src/tui/engine.mjs +569 -948
  247. package/src/tui/index.jsx +117 -7
  248. package/src/tui/input-editing.mjs +58 -8
  249. package/src/tui/input-editing.selection.test.mjs +75 -0
  250. package/src/tui/keyboard-protocol.mjs +42 -0
  251. package/src/tui/lib/voice-recorder.mjs +469 -0
  252. package/src/tui/markdown/format-token.mjs +128 -76
  253. package/src/tui/markdown/format-token.test.mjs +61 -19
  254. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  255. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  256. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  257. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  258. package/src/tui/markdown/table-layout.mjs +9 -9
  259. package/src/tui/paste-attachments.mjs +36 -9
  260. package/src/tui/paste-fix.test.mjs +119 -0
  261. package/src/tui/prompt-history-store.mjs +129 -0
  262. package/src/tui/prompt-history-store.test.mjs +52 -0
  263. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  264. package/src/tui/theme.mjs +41 -657
  265. package/src/tui/themes/base.mjs +86 -0
  266. package/src/tui/themes/basic.mjs +85 -0
  267. package/src/tui/themes/catppuccin.mjs +72 -0
  268. package/src/tui/themes/dracula.mjs +70 -0
  269. package/src/tui/themes/everforest.mjs +71 -0
  270. package/src/tui/themes/gruvbox.mjs +71 -0
  271. package/src/tui/themes/index.mjs +71 -0
  272. package/src/tui/themes/indigo.mjs +78 -0
  273. package/src/tui/themes/kanagawa.mjs +80 -0
  274. package/src/tui/themes/light.mjs +81 -0
  275. package/src/tui/themes/nord.mjs +72 -0
  276. package/src/tui/themes/onedark.mjs +16 -0
  277. package/src/tui/themes/rosepine.mjs +70 -0
  278. package/src/tui/themes/teal.mjs +80 -0
  279. package/src/tui/themes/tokyonight.mjs +79 -0
  280. package/src/tui/themes/utils.mjs +106 -0
  281. package/src/tui/themes/warm.mjs +79 -0
  282. package/src/tui/transcript-tool-failures.mjs +13 -2
  283. package/src/ui/markdown.mjs +1 -1
  284. package/src/ui/model-display.mjs +2 -2
  285. package/src/ui/statusline.mjs +75 -27
  286. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  287. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  288. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  289. package/src/workflows/default/WORKFLOW.md +46 -12
  290. package/src/workflows/sequential/WORKFLOW.md +51 -0
  291. package/src/workflows/solo/WORKFLOW.md +12 -1
  292. package/vendor/ink/build/display-width.js +62 -0
  293. package/vendor/ink/build/ink.js +100 -12
  294. package/vendor/ink/build/measure-text.js +4 -1
  295. package/vendor/ink/build/output.js +115 -9
  296. package/vendor/ink/build/render-node-to-output.js +4 -1
  297. package/vendor/ink/build/render.js +4 -0
  298. package/src/output-styles/extreme-simple.md +0 -20
  299. package/src/rules/lead/04-workflow.md +0 -51
  300. package/src/workflows/default/workflow.json +0 -13
  301. package/src/workflows/solo/workflow.json +0 -7
@@ -29,13 +29,17 @@ import {
29
29
  traceAgentUsage,
30
30
  } from '../agent-trace.mjs';
31
31
  import {
32
- PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
33
32
  PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
33
+ PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
34
+ PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
35
+ streamStalledError,
34
36
  createTimeoutSignal,
37
+ createPassthroughSignal,
35
38
  } from '../stall-policy.mjs';
36
39
  import { populateHttpStatusFromMessage, shouldFallbackTransport } from './retry-classifier.mjs';
37
40
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
38
41
  import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
42
+ import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
39
43
  import {
40
44
  normalizeContentForOpenAIResponses,
41
45
  splitToolContentForOpenAIResponses,
@@ -52,14 +56,13 @@ const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
52
56
  const CODEX_OAUTH_ORIGINATOR = 'codex_cli_rs';
53
57
  const TOKEN_URL = 'https://auth.openai.com/oauth/token';
54
58
  const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
55
- // Version string baked into the models endpoint query — the OAuth backend rejects the
56
- // request without it. Keep close to the latest published @openai/codex CLI because
57
- // older versions trigger a visibility-filtered catalog (e.g. only rollout
58
- // models). Bump when the real CLI bumps.
59
- // OpenAI OAuth backend gates new model exposures (e.g. gpt-5.5 only on >= 0.130.0)
60
- // on the client_version header. Resolve dynamically from npm so newly-shipped
61
- // models surface within a day instead of waiting on a hardcoded bump here.
62
- // Cached 24h in-process; npm failure falls back to the floor below.
59
+ // Version string baked into the models endpoint query — the OAuth backend
60
+ // rejects the request without it, and gates new model exposures (e.g.
61
+ // gpt-5.5 only on >= 0.130.0) on this client_version header; older versions
62
+ // trigger a visibility-filtered catalog (e.g. only rollout models). Resolved
63
+ // dynamically from npm so newly-shipped models surface within a day instead
64
+ // of waiting on a hardcoded bump here. Cached 24h in-process; npm failure
65
+ // falls back to the floor below.
63
66
  const CODEX_CLIENT_VERSION_FLOOR = '0.130.0';
64
67
  const CODEX_VERSION_CACHE_TTL_MS = 24 * 60 * 60_000;
65
68
  let _codexVersionCache = { value: null, fetchedAt: 0 };
@@ -160,6 +163,17 @@ function _displayCodexModel(id) {
160
163
  return id.replace(/-\d{4}-\d{2}-\d{2}$/, '');
161
164
  }
162
165
 
166
+ function _positiveCodexContextWindow(value) {
167
+ const n = Number(value);
168
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
169
+ }
170
+
171
+ function _codexContextWindowFromApi(m) {
172
+ return _positiveCodexContextWindow(m?.context_window)
173
+ || _positiveCodexContextWindow(m?.max_context_window)
174
+ || null;
175
+ }
176
+
163
177
  function _normalizeCodexModel(m) {
164
178
  const id = m?.slug || m?.id;
165
179
  const family = _codexFamily(id);
@@ -182,8 +196,8 @@ function _normalizeCodexModel(m) {
182
196
  display: m?.display_name || id,
183
197
  family,
184
198
  provider: 'openai-oauth',
185
- contextWindow: m?.context_window || m?.max_context_window || 1000000,
186
- maxContextWindow: m?.max_context_window || null,
199
+ contextWindow: _codexContextWindowFromApi(m),
200
+ maxContextWindow: _positiveCodexContextWindow(m?.max_context_window),
187
201
  outputTokens: m?.max_output_tokens || m?.output_tokens || 32768,
188
202
  autoCompactTokenLimit: m?.auto_compact_token_limit || null,
189
203
  effectiveContextWindowPercent: m?.effective_context_window_percent || null,
@@ -572,7 +586,7 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
572
586
  providerState: opts.providerState,
573
587
  model,
574
588
  });
575
- // Match the body shape pi-mono and the official OpenAI CLI ship so the
589
+ // Match the request body shape the OAuth backend expects so the
576
590
  // server-side auto-cache routes correctly. text.verbosity / include /
577
591
  // tool_choice / parallel_tool_calls are all inert without side effects
578
592
  // for most callers but their presence affects how the OAuth backend classifies the
@@ -603,8 +617,8 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
603
617
  if (opts.fast === true) {
604
618
  // 'priority' is the only fast-class value the OpenAI OAuth backend
605
619
  // accepts on the wire: 'fast' is hard-rejected ("Unsupported
606
- // service_tier: fast", probed 2026-06-11). Match official CLI behavior:
607
- // only send the request value when the model catalog advertises it.
620
+ // service_tier: fast", probed 2026-06-11). Only send the request value
621
+ // when the model catalog advertises it.
608
622
  if (codexModelSupportsServiceTier(model, 'priority')) {
609
623
  body.service_tier = 'priority';
610
624
  }
@@ -654,8 +668,8 @@ function _envPositiveInt(name, fallback) {
654
668
  }
655
669
 
656
670
  // Completed function_call.arguments parse for the OpenAI Responses stream.
657
- // Native convergence (openai-oauth / anthropic-oauth / opencode): a function_call item
658
- // arrives only on a completion/done signal, so a non-empty-but-malformed
671
+ // A function_call item arrives only on a completion/done signal, so a
672
+ // non-empty-but-malformed
659
673
  // arguments string is deterministic bad JSON — NOT mid-stream truncation.
660
674
  // Empty/whitespace input legitimately means "no arguments" → {}. A non-empty
661
675
  // string that fails JSON.parse is surfaced as an invalid-args MARKER (instead
@@ -773,11 +787,20 @@ export async function sendViaHttpSse({
773
787
  useModel,
774
788
  fetchFn = fetch,
775
789
  } = {}) {
776
- const totalTimeout = createTimeoutSignal(
777
- externalSignal,
778
- PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
779
- 'OpenAI OAuth HTTP fallback total',
780
- );
790
+ // P1 audit fix: no fixed wall-clock total cap on the HTTP/SSE fallback
791
+ // stream. The old createTimeoutSignal(..., PROVIDER_GENERATE_TOTAL_TIMEOUT_MS)
792
+ // killed a healthy, still-streaming turn purely on elapsed time, unlike
793
+ // every other streaming provider path (anthropic-oauth uses the same
794
+ // createPassthroughSignal pattern — see anthropic-oauth.mjs "Option A").
795
+ // The stream is bounded instead by:
796
+ // (a) headerTimeout below (PROVIDER_HTTP_RESPONSE_TIMEOUT_MS) for a
797
+ // socket that never sends the initial response,
798
+ // (b) the SEMANTIC idle watchdog (_armSemanticIdle /
799
+ // PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS), which resets on every
800
+ // meaningful() chunk — a live stream stays alive, a truly silent
801
+ // one still aborts, and
802
+ // (c) externalSignal (client disconnect / replaced-by-newer-request).
803
+ const totalTimeout = createPassthroughSignal(externalSignal);
781
804
  const headerTimeout = createTimeoutSignal(
782
805
  totalTimeout.signal,
783
806
  PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
@@ -851,6 +874,33 @@ export async function sendViaHttpSse({
851
874
  if (totalTimeout.signal.aborted) _onTotalAbort();
852
875
  else totalTimeout.signal.addEventListener('abort', _onTotalAbort, { once: true });
853
876
  }
877
+ // SEMANTIC idle watchdog: reset ONLY on meaningful() (text/reasoning/tool
878
+ // deltas), never on raw bytes/keepalive frames, so a stream that emits some
879
+ // deltas then goes silent trips a short, named terminal failure instead of
880
+ // hanging until the 30-min agent watchdog. Disablable via the shared env.
881
+ let _semanticIdleTimer = null;
882
+ const _clearSemanticIdle = () => {
883
+ if (_semanticIdleTimer) { clearTimeout(_semanticIdleTimer); _semanticIdleTimer = null; }
884
+ };
885
+ const _armSemanticIdle = () => {
886
+ if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED || !(PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS > 0)) return;
887
+ _clearSemanticIdle();
888
+ _semanticIdleTimer = setTimeout(() => {
889
+ _streamAbortReason = streamStalledError('OpenAI OAuth HTTP fallback', PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS, { emittedToolCall: emittedToolCallIds.size > 0 });
890
+ // Partial-final recovery: attach the
891
+ // streamed partial state so the agent loop can accept a wedged FINAL
892
+ // no-tool summary as a successful partial-final instead of dropping
893
+ // the result. pendingToolUse gates out any mid-flight tool call.
894
+ try {
895
+ _streamAbortReason.partialContent = content;
896
+ _streamAbortReason.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
897
+ _streamAbortReason.pendingToolUse = pendingCalls.size > 0 || emittedToolCallIds.size > 0;
898
+ _streamAbortReason.partialModel = model || undefined;
899
+ } catch { /* best-effort enrichment */ }
900
+ try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
901
+ }, PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS);
902
+ try { _semanticIdleTimer.unref?.(); } catch {}
903
+ };
854
904
  let buffer = '';
855
905
  let content = '';
856
906
  let model = '';
@@ -885,13 +935,75 @@ export async function sendViaHttpSse({
885
935
  // first complete frame still emits; only redundant re-emits are
886
936
  // suppressed.
887
937
  const emittedToolCallIds = new Set();
938
+ // Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
939
+ // identical native function_call must fire onToolCall exactly once.
940
+ const _toolDedupe = createToolCallDedupe();
888
941
  const emitToolCall = (call) => {
889
942
  if (!call || !call.id) return;
890
943
  if (emittedToolCallIds.has(call.id)) return;
891
944
  emittedToolCallIds.add(call.id);
945
+ if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
892
946
  try { onToolCall?.(call); } catch {}
893
947
  };
894
948
 
949
+ // Leaked tool-call guard. The model sometimes emits a tool call as plain
950
+ // text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
951
+ // `<|channel|>...to=functions.NAME...<|call|>`) inside
952
+ // `response.output_text.delta` instead of a native function_call. Route
953
+ // text through the guard so leaked calls are suppressed from the visible
954
+ // stream, synthesized (native `call_...` id shape), and dispatched like
955
+ // native ones. Known tool names come from the request body so recovery
956
+ // only fires for tools the model was actually offered. Additive: the
957
+ // native function_call path is untouched.
958
+ const _leakKnownTools = new Set(
959
+ (Array.isArray(body?.tools) ? body.tools : [])
960
+ .map((t) => (typeof t?.name === 'string' ? t.name : null))
961
+ .filter(Boolean),
962
+ );
963
+ const leakGuard = createLeakGuard({ knownToolNames: _leakKnownTools, harmony: true });
964
+ const dispatchLeakedCall = (recovered) => {
965
+ let args = recovered?.arguments;
966
+ if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
967
+ const call = {
968
+ id: `call_leaked_${randomBytes(8).toString('hex')}`,
969
+ name: recovered.name,
970
+ arguments: args,
971
+ };
972
+ toolCalls.push(call);
973
+ emitToolCall(call);
974
+ };
975
+ const relayLeakText = (delta) => {
976
+ if (!leakGuard.enabled) {
977
+ content += delta || '';
978
+ if (delta && onTextDelta) {
979
+ emittedText = true;
980
+ try { onTextDelta(delta); } catch {}
981
+ }
982
+ return;
983
+ }
984
+ const { text, calls } = leakGuard.push(delta);
985
+ if (text) {
986
+ content += text;
987
+ if (onTextDelta) {
988
+ emittedText = true;
989
+ try { onTextDelta(text); } catch {}
990
+ }
991
+ }
992
+ for (const c of calls) dispatchLeakedCall(c);
993
+ };
994
+ const flushLeak = () => {
995
+ if (!leakGuard.enabled) return;
996
+ const { text, calls } = leakGuard.flush();
997
+ if (text) {
998
+ content += text;
999
+ if (onTextDelta) {
1000
+ emittedText = true;
1001
+ try { onTextDelta(text); } catch {}
1002
+ }
1003
+ }
1004
+ for (const c of calls) dispatchLeakedCall(c);
1005
+ };
1006
+
895
1007
  const pushWebSearchCall = (item) => {
896
1008
  if (!item || item.type !== 'web_search_call') return;
897
1009
  const key = item.id || JSON.stringify(item.action || item);
@@ -939,6 +1051,7 @@ export async function sendViaHttpSse({
939
1051
  };
940
1052
  const meaningful = () => {
941
1053
  if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
1054
+ _armSemanticIdle();
942
1055
  try { onStreamDelta?.(); } catch {}
943
1056
  };
944
1057
  const handleEvent = (event) => {
@@ -949,12 +1062,8 @@ export async function sendViaHttpSse({
949
1062
  if (event.response?.id) responseId = event.response.id;
950
1063
  break;
951
1064
  case 'response.output_text.delta':
952
- content += event.delta || '';
953
1065
  meaningful();
954
- if (event.delta && onTextDelta) {
955
- emittedText = true;
956
- try { onTextDelta(event.delta); } catch {}
957
- }
1066
+ relayLeakText(event.delta || '');
958
1067
  break;
959
1068
  case 'response.reasoning_text.delta':
960
1069
  case 'response.reasoning_summary_text.delta':
@@ -966,6 +1075,22 @@ export async function sendViaHttpSse({
966
1075
  name: event.item.name || '',
967
1076
  callId: event.item.call_id || '',
968
1077
  });
1078
+ } else if (event.item?.type === 'tool_search_call') {
1079
+ // Mark tool_search as in-flight the moment the item is
1080
+ // added, mirroring function_call above, so the semantic
1081
+ // idle watchdog's pendingToolUse gate (pendingCalls.size)
1082
+ // sees a mid-flight tool_search and never lets stall
1083
+ // recovery drop it before response.output_item.done.
1084
+ // kind:'tool_search' tags the entry so the shared
1085
+ // function_call_arguments.done handler (below) never
1086
+ // mistakes it for a function call by id collision/empty id.
1087
+ if (event.item.id) {
1088
+ pendingCalls.set(event.item.id, {
1089
+ name: 'tool_search',
1090
+ callId: event.item.call_id || '',
1091
+ kind: 'tool_search',
1092
+ });
1093
+ }
969
1094
  }
970
1095
  break;
971
1096
  case 'response.function_call_arguments.delta':
@@ -974,6 +1099,7 @@ export async function sendViaHttpSse({
974
1099
  case 'response.function_call_arguments.done': {
975
1100
  const itemId = event.item_id || '';
976
1101
  const pending = pendingCalls.get(itemId);
1102
+ if (pending?.kind === 'tool_search') { meaningful(); break; }
977
1103
  const call = {
978
1104
  id: pending?.callId || event.call_id || '',
979
1105
  name: pending?.name || event.name || '',
@@ -1006,6 +1132,7 @@ export async function sendViaHttpSse({
1006
1132
  }
1007
1133
  }
1008
1134
  } else if (item.type === 'tool_search_call') {
1135
+ pendingCalls.delete(item.id || '');
1009
1136
  pushToolSearchCall(item);
1010
1137
  } else if (item.type === 'custom_tool_call') {
1011
1138
  pushCustomToolCall(item);
@@ -1030,7 +1157,20 @@ export async function sendViaHttpSse({
1030
1157
  for (const item of resp.output || []) {
1031
1158
  if (item.type === 'message') {
1032
1159
  for (const part of item.content || []) {
1033
- if (!content && part.type === 'output_text') content += part.text || '';
1160
+ if (!content && part.type === 'output_text') {
1161
+ // Completed-output fallback (no streamed text).
1162
+ // Route through the leak guard so a tool call
1163
+ // leaked only in the final bundle is recovered
1164
+ // rather than surfaced as visible content. push
1165
+ // with final=true flushes fully (no held tail).
1166
+ if (leakGuard.enabled) {
1167
+ const { text, calls } = leakGuard.push(part.text || '', true);
1168
+ content += text;
1169
+ for (const c of calls) dispatchLeakedCall(c);
1170
+ } else {
1171
+ content += part.text || '';
1172
+ }
1173
+ }
1034
1174
  if (part.type === 'output_text') _pushOutputTextAnnotations(part, citations, citationKeys);
1035
1175
  }
1036
1176
  } else if (item.type === 'reasoning') {
@@ -1116,10 +1256,12 @@ export async function sendViaHttpSse({
1116
1256
 
1117
1257
  try {
1118
1258
  while (true) {
1119
- if (totalTimeout.signal.aborted) {
1259
+ if (totalTimeout.signal?.aborted) {
1260
+ _clearSemanticIdle();
1120
1261
  const reason = totalTimeout.signal.reason;
1121
1262
  throw reason instanceof Error ? reason : new Error('OpenAI OAuth HTTP fallback aborted');
1122
1263
  }
1264
+ if (_streamAbortReason) throw _streamAbortReason;
1123
1265
  const { value, done } = await reader.read();
1124
1266
  if (done) break;
1125
1267
  buffer += decoder.decode(value, { stream: true });
@@ -1140,12 +1282,16 @@ export async function sendViaHttpSse({
1140
1282
  const event = _parseSseFrame(frame);
1141
1283
  if (event) handleEvent(event);
1142
1284
  }
1285
+ // Flush any partial-sentinel tail held back mid-stream so legitimate
1286
+ // trailing text is never lost (streamed-text path).
1287
+ flushLeak();
1143
1288
  } catch (err) {
1144
1289
  // Live-text invariant: once a non-empty chunk has been relayed it
1145
1290
  // cannot be withdrawn — flag the error so no upstream layer retries.
1146
1291
  if (emittedText && err) { try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {} }
1147
1292
  throw err;
1148
1293
  } finally {
1294
+ _clearSemanticIdle();
1149
1295
  try { reader.releaseLock?.(); } catch {}
1150
1296
  if (_onTotalAbort && totalTimeout.signal) {
1151
1297
  try { totalTimeout.signal.removeEventListener('abort', _onTotalAbort); } catch {}
@@ -1186,15 +1332,27 @@ export async function sendViaHttpSse({
1186
1332
  serviceTier,
1187
1333
  });
1188
1334
  }
1335
+ // Dedupe the returned array by name+args (Fix 2, array side): a synthetic
1336
+ // leaked call and an identical native function_call must not both survive,
1337
+ // else the agent loop executes the side-effecting tool twice.
1338
+ const _returnedToolCalls = toolCalls.length
1339
+ ? dedupeToolCallList(toolCalls.map(({ _pendingItemId, ...t }) => t))
1340
+ : undefined;
1189
1341
  return {
1190
1342
  content,
1191
1343
  model: liveModel,
1192
1344
  reasoningItems: reasoningItems.length ? reasoningItems : undefined,
1193
- toolCalls: toolCalls.length ? toolCalls.map(({ _pendingItemId, ...t }) => t) : undefined,
1345
+ toolCalls: _returnedToolCalls,
1194
1346
  citations: citations.length ? citations : undefined,
1195
1347
  webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
1196
1348
  usage: usage || undefined,
1197
1349
  stopReason: stopReason || undefined,
1350
+ // P1 audit fix: text-only max-output cutoff (openai-oauth HTTP/SSE
1351
+ // fallback maps status:'incomplete'/reason=max_output_tokens to
1352
+ // stopReason='length' above and treats it as success). Flag it so
1353
+ // loop.mjs can surface a truncation warning instead of accepting
1354
+ // silently-cut content as a clean final answer.
1355
+ ...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
1198
1356
  responseId: responseId || undefined,
1199
1357
  serviceTier: serviceTier || undefined,
1200
1358
  };
@@ -1328,8 +1486,8 @@ export class OpenAIOAuthProvider {
1328
1486
  const onTextDelta = typeof opts.onTextDelta === 'function' ? opts.onTextDelta : null;
1329
1487
  const externalSignal = opts.signal || null;
1330
1488
  const _sendSessionId = opts.sessionId || '(none)';
1331
- const _sendRole = opts.role || '(none)';
1332
- if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-start sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} role=${_sendRole} expiringInMs=${this.tokens?.expires_at ? this.tokens.expires_at - Date.now() : 'unknown'}\n`); }
1489
+ const _sendAgent = opts.agent || '(none)';
1490
+ if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-start sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} agent=${_sendAgent} expiringInMs=${this.tokens?.expires_at ? this.tokens.expires_at - Date.now() : 'unknown'}\n`); }
1333
1491
  // Build request body in parallel with auth resolution. ensureAuth is
1334
1492
  // a no-op fast-path on cached tokens, but a refresh round-trip can
1335
1493
  // take 300ms+; the body build (message serialisation) overlaps cleanly.
@@ -1389,6 +1547,26 @@ export class OpenAIOAuthProvider {
1389
1547
  this._forceHttpFallback = true;
1390
1548
  this._forceHttpFallbackUntil = Date.now() + ttlMs;
1391
1549
  };
1550
+ const traceWsError = (err, stage = 'primary') => {
1551
+ try {
1552
+ appendAgentTrace({
1553
+ sessionId: poolKey,
1554
+ iteration,
1555
+ kind: 'transport_error',
1556
+ provider: 'openai-oauth',
1557
+ model: useModel,
1558
+ transport: 'websocket',
1559
+ payload: {
1560
+ stage,
1561
+ error_code: err?.code || null,
1562
+ error_http_status: Number(err?.httpStatus || 0) || null,
1563
+ error_classifier: err?.retryClassifier || err?.midstreamClassifier || null,
1564
+ live_text_emitted: err?.liveTextEmitted === true || err?.unsafeToRetry === true,
1565
+ message: String(err?.message || err || '').slice(0, 500),
1566
+ },
1567
+ });
1568
+ } catch {}
1569
+ };
1392
1570
  const dispatchHttp = async (reason, originalErr = null, { sticky = false } = {}) => {
1393
1571
  appendAgentTrace({
1394
1572
  sessionId: poolKey,
@@ -1411,7 +1589,7 @@ export class OpenAIOAuthProvider {
1411
1589
  process.stderr.write('[openai-oauth] WebSocket bypassed (forced); using HTTP/SSE\n');
1412
1590
  }
1413
1591
  } else {
1414
- process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
1592
+ if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
1415
1593
  }
1416
1594
  const result = await sendHttp({
1417
1595
  auth,
@@ -1459,11 +1637,12 @@ export class OpenAIOAuthProvider {
1459
1637
  // Prefer WebSocket for hot cache/delta transport; fall back to HTTP/SSE
1460
1638
  // after retry-exhausted handshake/acquire/no-first-event failures.
1461
1639
  try {
1462
- if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-start model=${useModel} role=${_sendRole} sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} iteration=${iteration ?? '(none)'}\n`); }
1640
+ if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-start model=${useModel} agent=${_sendAgent} sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} iteration=${iteration ?? '(none)'}\n`); }
1463
1641
  const result = await dispatchWs(false);
1464
1642
  if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
1465
1643
  return recordLiveModel(result);
1466
1644
  } catch (err) {
1645
+ traceWsError(err, 'primary');
1467
1646
  const status = err?.httpStatus;
1468
1647
  // Live-text invariant: if the WS attempt already relayed a
1469
1648
  // non-empty text chunk to the client, NO recovery path may reissue
@@ -1482,6 +1661,7 @@ export class OpenAIOAuthProvider {
1482
1661
  if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
1483
1662
  return recordLiveModel(result);
1484
1663
  } catch (retryErr) {
1664
+ traceWsError(retryErr, 'auth_retry');
1485
1665
  if (_shouldUseOpenAIHttpFallback(retryErr, externalSignal)) {
1486
1666
  try {
1487
1667
  return await dispatchHttp(
@@ -71,6 +71,24 @@ export class OpenAIDirectProvider {
71
71
  // so gpt-5.4-mini can opt into Priority even when the OAuth catalog does
72
72
  // not advertise a Fast tier for its OAuth endpoint.
73
73
  applyOpenAIDirectFastTier(body, useModel, opts);
74
+ // P0 audit fix: buildRequestBody (openai-oauth.mjs) defaults
75
+ // store:false (env-gated, MIXDOG_OAI_STORE), which is correct for
76
+ // the openai-oauth ChatGPT-subscription backend — that backend keeps
77
+ // its own conversation state via the WS handshake session_id
78
+ // (see openai-oauth-ws.mjs "conversation slot ... in-memory prefix
79
+ // state"), independent of the public Responses API `store` field.
80
+ // The public OpenAI direct WS path below, however, talks to the real
81
+ // api.openai.com Responses API, where `previous_response_id`
82
+ // continuation is only valid when the anchored response was actually
83
+ // stored — store:false + previous_response_id is a broken
84
+ // combination there (the server has nothing to look up). This
85
+ // provider's WS transport always injects previous_response_id via
86
+ // openai-oauth-ws.mjs's delta path once a response id is cached, so
87
+ // force store:true here — same override xAI's Responses path takes
88
+ // (see openai-compat.mjs _doSendXaiResponses/_doSendXaiResponsesWebSocket:
89
+ // "the public endpoint currently returns previous_response_not_found
90
+ // ... unless the chain is stored").
91
+ body.store = true;
74
92
  // Public Responses API supports prompt_cache_retention='24h' at no
75
93
  // extra cost (same cached_input_tokens billing as the default 5–10
76
94
  // min in-memory cache). openai-oauth rejects the parameter, so it's
@@ -1,8 +1,9 @@
1
- import { existsSync, readFileSync, mkdirSync } from 'node:fs';
2
- import * as fsp from 'node:fs/promises';
1
+ import { existsSync, readFileSync } from 'node:fs';
3
2
  import { join } from 'node:path';
3
+ import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
4
4
  import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
5
5
  import { getOpenCodeGoAuthCookie } from '../../../shared/config.mjs';
6
+ import { round, cleanString as clean } from './lib/usage-primitives.mjs';
6
7
 
7
8
  const CACHE_FILE = 'opencode-go-usage-cache.json';
8
9
  const LIVE_TTL_MS = 5 * 60_000;
@@ -18,23 +19,14 @@ const LIMITS_USD = Object.freeze({
18
19
  const DISK_JSON_MEMORY_TTL_MS = 1000;
19
20
  let diskJsonCache = { at: 0, file: '', value: null };
20
21
 
22
+ // Local unguarded `num`: this module intentionally coerces '' to 0 via
23
+ // Number(''), unlike the guarded shared num() in lib/usage-primitives.mjs.
24
+ // Behavior differs on empty-string input, so it stays local.
21
25
  function num(value, fallback = null) {
22
26
  const n = Number(value);
23
27
  return Number.isFinite(n) ? n : fallback;
24
28
  }
25
29
 
26
- function round(value, digits = 4) {
27
- const n = Number(value);
28
- if (!Number.isFinite(n)) return null;
29
- const scale = 10 ** digits;
30
- return Math.round(n * scale) / scale;
31
- }
32
-
33
- function clean(value) {
34
- const text = typeof value === 'string' ? value.trim() : '';
35
- return text || null;
36
- }
37
-
38
30
  function cachePath() {
39
31
  return join(resolvePluginData(), CACHE_FILE);
40
32
  }
@@ -53,12 +45,18 @@ function readJson(file) {
53
45
  }
54
46
  }
55
47
 
48
+ // Synchronous atomic+lock write (updateJsonAtomicSync) instead of the prior
49
+ // fire-and-forget fsp.writeFile: this cache is single-entry (one snapshot
50
+ // per file, no cross-process merge), so the lock protects against a torn
51
+ // write racing readers, not a lost-update merge. Only one write happens
52
+ // per successful fetch (TTL-gated, at most once per LIVE_TTL_MS), so the
53
+ // switch off async has no meaningful latency impact on the request path.
56
54
  function writeJson(file, value) {
57
- diskJsonCache = { at: Date.now(), file, value };
55
+ let next = null;
58
56
  try {
59
- mkdirSync(resolvePluginData(), { recursive: true });
60
- void fsp.writeFile(file, JSON.stringify(value, null, 2), 'utf8').catch(() => {});
57
+ next = updateJsonAtomicSync(file, () => value, { lock: true, fsyncDir: true, timeoutMs: 1000 }); // best-effort cache write: short lock timeout, don't block on contention
61
58
  } catch {}
59
+ if (next) diskJsonCache = { at: Date.now(), file, value: next }; // only mirror on confirmed write, avoid phantom cache on lock timeout
62
60
  }
63
61
 
64
62
  function freshSnapshot(snapshot, ttlMs) {
@@ -243,7 +241,41 @@ export function openCodeGoUsageConfigStatus(config = {}) {
243
241
  };
244
242
  }
245
243
 
244
+ // Primary discovery: GET /auth with the auth cookie, follow-manual. The
245
+ // console redirects authenticated sessions straight to /workspace/{id};
246
+ // unauthenticated/invalid cookies redirect to /auth/authorize instead.
247
+ // This avoids depending on the hashed server-fn id used by the /_server
248
+ // probe (WORKSPACES_SERVER_ID), which can change across console deploys.
249
+ async function fetchWorkspaceIdFromAuthRedirect(authCookie, { signal } = {}) {
250
+ let res;
251
+ try {
252
+ res = await fetch(`${BASE_URL}/auth`, {
253
+ signal,
254
+ redirect: 'manual',
255
+ headers: requestHeaders(authCookie),
256
+ });
257
+ } catch {
258
+ return null; // network/redirect-mode quirk: let the _server fallback decide
259
+ }
260
+ if (res.status === 401 || res.status === 403) {
261
+ const err = new Error('OpenCode Go console auth failed');
262
+ err.code = 'OPENCODE_GO_USAGE_AUTH_FAILED';
263
+ throw err;
264
+ }
265
+ const location = res.headers.get('location') || '';
266
+ if (!location) return null;
267
+ if (/(?:^|\/|\.\/)auth\/authorize\b/.test(location)) {
268
+ const err = new Error('OpenCode Go console auth failed');
269
+ err.code = 'OPENCODE_GO_USAGE_AUTH_FAILED';
270
+ throw err;
271
+ }
272
+ const workspaceMatch = location.match(/\/workspace\/(wrk_[a-zA-Z0-9]+)/);
273
+ return normalizeWorkspaceId(workspaceMatch ? workspaceMatch[1] : location);
274
+ }
275
+
246
276
  async function fetchWorkspaceId(authCookie, { signal } = {}) {
277
+ const fromRedirect = await fetchWorkspaceIdFromAuthRedirect(authCookie, { signal });
278
+ if (fromRedirect) return fromRedirect;
247
279
  const url = new URL(`${BASE_URL}/_server`);
248
280
  url.searchParams.set('id', WORKSPACES_SERVER_ID);
249
281
  const res = await fetch(url, {
@@ -201,7 +201,8 @@ export function getProvider(name) {
201
201
  // unregistered providers default to false (the openai/gemini majority).
202
202
  export function providerInputExcludesCache(name) {
203
203
  const p = getProvider(name);
204
- return p?.constructor?.inputExcludesCache === true;
204
+ if (p?.constructor?.inputExcludesCache === true) return true;
205
+ return String(name || '').toLowerCase().includes('anthropic');
205
206
  }
206
207
  export function getAllProviders() {
207
208
  // Defensive copy — callers must not mutate the live registry or retain