mixdog 0.9.87 → 0.9.89

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 (167) hide show
  1. package/package.json +13 -7
  2. package/scripts/tool-failures.mjs +60 -24
  3. package/src/agents/heavy-worker/AGENT.md +3 -4
  4. package/src/agents/worker/AGENT.md +1 -2
  5. package/src/cli.mjs +8 -0
  6. package/src/defaults/cycle3-review-prompt.md +4 -4
  7. package/src/rules/agent/00-core.md +3 -0
  8. package/src/rules/agent/42-cycle3-agent.md +5 -6
  9. package/src/rules/lead/01-general.md +2 -0
  10. package/src/rules/shared/01-tool.md +29 -35
  11. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
  12. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
  13. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
  16. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
  17. package/src/runtime/agent/orchestrator/config.mjs +29 -11
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
  21. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
  25. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
  26. package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
  27. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
  30. package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
  32. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
  35. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
  42. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
  43. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
  44. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
  45. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
  46. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
  49. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
  50. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
  51. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
  52. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
  53. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
  54. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
  55. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
  56. package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
  57. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
  58. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
  59. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
  60. package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
  61. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
  62. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
  63. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
  64. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
  65. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
  66. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
  67. package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
  68. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
  69. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
  70. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
  71. package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
  72. package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
  73. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
  74. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
  77. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
  81. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
  85. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
  86. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
  87. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
  88. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
  89. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
  90. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
  91. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
  92. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
  93. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  94. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
  95. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
  96. package/src/runtime/media/lanes.mjs +2 -2
  97. package/src/runtime/media/renditions.mjs +101 -17
  98. package/src/runtime/media/renditions.test.mjs +54 -0
  99. package/src/runtime/media/store.mjs +111 -43
  100. package/src/runtime/media/store.test.mjs +53 -11
  101. package/src/runtime/memory/index.mjs +26 -50
  102. package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
  103. package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
  104. package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
  105. package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
  106. package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
  107. package/src/runtime/memory/lib/http-router.mjs +4 -0
  108. package/src/runtime/memory/lib/ko-morph.mjs +49 -6
  109. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
  110. package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
  111. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
  112. package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
  113. package/src/runtime/memory/lib/query-handlers.mjs +26 -28
  114. package/src/runtime/memory/tool-defs.mjs +3 -3
  115. package/src/runtime/shared/atomic-file.mjs +5 -1
  116. package/src/runtime/shared/child-guardian.mjs +73 -2
  117. package/src/runtime/shared/provider-api-key.mjs +22 -5
  118. package/src/runtime/shared/tool-execution-contract.mjs +53 -0
  119. package/src/session-runtime/config-helpers.mjs +6 -5
  120. package/src/session-runtime/lifecycle-api.mjs +16 -4
  121. package/src/session-runtime/media-api.mjs +31 -16
  122. package/src/session-runtime/prewarm.mjs +2 -24
  123. package/src/session-runtime/runtime-core.mjs +33 -15
  124. package/src/session-runtime/runtime-tunables.mjs +0 -3
  125. package/src/session-runtime/session-lifecycle.mjs +0 -6
  126. package/src/session-runtime/session-title.mjs +228 -0
  127. package/src/session-runtime/session-turn-api.mjs +11 -7
  128. package/src/session-runtime/workflow-agents-api.mjs +16 -0
  129. package/src/session-runtime/workflow.mjs +7 -5
  130. package/src/standalone/agent-tool/job-views.mjs +90 -36
  131. package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
  132. package/src/standalone/agent-tool/worker-index.mjs +8 -0
  133. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  134. package/src/standalone/agent-tool.mjs +17 -17
  135. package/src/standalone/agent-watchdog-registry.mjs +19 -2
  136. package/src/standalone/channel-daemon.mjs +8 -0
  137. package/src/standalone/explore-tool.mjs +1 -1
  138. package/src/standalone/memory-runtime-proxy.mjs +4 -7
  139. package/src/standalone/projects.mjs +4 -1
  140. package/src/standalone/usage-dashboard.mjs +25 -3
  141. package/src/tui/App.jsx +5 -3
  142. package/src/tui/app/resume-picker.mjs +7 -2
  143. package/src/tui/app/route-pickers.mjs +11 -117
  144. package/src/tui/app/slash-dispatch.mjs +13 -3
  145. package/src/tui/app/transcript-row-estimate.mjs +1 -1
  146. package/src/tui/app/transcript-window.mjs +17 -93
  147. package/src/tui/app/use-transcript-scroll.mjs +32 -2
  148. package/src/tui/app/use-transcript-window.mjs +64 -114
  149. package/src/tui/components/Markdown.jsx +19 -2
  150. package/src/tui/components/Message.jsx +8 -3
  151. package/src/tui/components/ToolExecution.jsx +7 -3
  152. package/src/tui/dist/index.mjs +243 -223
  153. package/src/tui/engine/live-share.mjs +15 -5
  154. package/src/tui/engine/render-timing.mjs +2 -2
  155. package/src/tui/engine/session-api-ext.mjs +74 -4
  156. package/src/tui/engine/session-flow.mjs +3 -1
  157. package/src/tui/engine.mjs +2 -2
  158. package/src/tui/hooks/useSharedTick.mjs +0 -2
  159. package/src/tui/index.jsx +5 -5
  160. package/src/ui/statusline-agents.mjs +44 -11
  161. package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
  162. package/src/workflows/default/WORKFLOW.md +3 -3
  163. package/src/workflows/solo/WORKFLOW.md +5 -3
  164. package/src/runtime/media/index.mjs +0 -18
  165. package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
  166. package/src/standalone/agent-shard/shard-child.mjs +0 -300
  167. package/src/standalone/agent-shard/shard-pool.mjs +0 -443
@@ -80,13 +80,15 @@ function normalizeGeminiSdkStreamError(err, label) {
80
80
  }
81
81
 
82
82
  // CC-rule safety stamp for Gemini stream failures (provider-stall audit):
83
- // once text has been relayed to the live gateway or a leaked tool call was
84
- // dispatched, replaying the request would double-render/double-execute — the
83
+ // once text has actually been RELAYED to the live gateway or a leaked tool
84
+ // call was dispatched, replaying would double-render/double-execute — the
85
85
  // outer withRetry() in gemini.mjs wraps the WHOLE stream, and a bare
86
86
  // EGEMINITIMEOUT classifies transient, so without these markers a mid-stream
87
- // stall after visible output was silently retried. Visible-text stalls also
88
- // gain streamStalled + partialContent so the loop's partial-final path can
89
- // keep the streamed output instead of dropping the turn.
87
+ // stall after visible output was silently retried. Text that was only
88
+ // BUFFERED (no onTextDelta sink, or still held inside the leak guard) was
89
+ // never shown, so it is not a replay boundary. Relayed-text stalls also gain
90
+ // streamStalled + partialContent so the loop's partial-final path can keep
91
+ // the streamed output instead of dropping the turn.
90
92
  function stampGeminiStreamFailure(err, {
91
93
  relayedText = '',
92
94
  textLeakGuard = null,
@@ -95,7 +97,7 @@ function stampGeminiStreamFailure(err, {
95
97
  } = {}) {
96
98
  if (!err || typeof err !== 'object') return err;
97
99
  const leaked = (textLeakGuard?.getLeakedToolCalls?.() || []).length > 0;
98
- const finalizedText = textLeakGuard?.getVisibleText?.();
100
+ const finalizedText = textLeakGuard?.getRelayedText?.();
99
101
  const visibleText = typeof finalizedText === 'string' ? finalizedText : relayedText;
100
102
  const visible = visibleText.length > 0;
101
103
  // Native functionCall chunks are only dispatched after successful stream
@@ -205,10 +207,13 @@ export function geminiChunkText(chunk) {
205
207
  return text;
206
208
  }
207
209
 
210
+ // Returns the text actually RELAYED to the live sink ('' when it was only
211
+ // buffered), so callers can track visible output rather than raw stream text.
208
212
  function relayGeminiStreamText(t, { onTextDelta, textLeakGuard }) {
209
- if (!t) return;
210
- if (textLeakGuard) textLeakGuard.feedText(t);
211
- else if (onTextDelta) { try { onTextDelta(t); } catch {} }
213
+ if (!t) return '';
214
+ if (textLeakGuard) { textLeakGuard.feedText(t); return ''; }
215
+ if (onTextDelta) { try { onTextDelta(t); } catch {} return t; }
216
+ return '';
212
217
  }
213
218
 
214
219
  /**
@@ -223,7 +228,7 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
223
228
  const _enabled = _knownTools.size > 0;
224
229
  const _isKnownTool = (name) => _knownTools.has(name);
225
230
  let leakBuffer = '';
226
- let visibleText = '';
231
+ let relayedText = '';
227
232
  const leakedCalls = [];
228
233
  const dispatchedFingerprints = new Set();
229
234
 
@@ -259,11 +264,12 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
259
264
  if (!leakBuffer && !final) return;
260
265
  const { emit, calls, rest } = scanLeakedToolCalls(leakBuffer, { isKnownTool: _isKnownTool, final });
261
266
  leakBuffer = rest;
267
+ // Only text handed to the live sink counts as relayed/visible. Without
268
+ // an onTextDelta sink the scrubbed text is buffered for the final
269
+ // result and never shown, so it must not deny a replay.
262
270
  if (emit && onTextDelta) {
263
- visibleText += emit;
271
+ relayedText += emit;
264
272
  try { onTextDelta(emit); } catch {}
265
- } else if (emit) {
266
- visibleText += emit;
267
273
  }
268
274
  for (const c of calls) dispatchLeakedCall(c);
269
275
  };
@@ -273,8 +279,10 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
273
279
  feedText(text) {
274
280
  if (!text) return;
275
281
  if (!_enabled) {
276
- visibleText += text;
277
- try { onTextDelta?.(text); } catch {}
282
+ if (onTextDelta) {
283
+ relayedText += text;
284
+ try { onTextDelta(text); } catch {}
285
+ }
278
286
  return;
279
287
  }
280
288
  leakBuffer += text;
@@ -304,8 +312,8 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
304
312
  getLeakedToolCalls() {
305
313
  return leakedCalls.length ? [...leakedCalls] : [];
306
314
  },
307
- getVisibleText() {
308
- return visibleText;
315
+ getRelayedText() {
316
+ return relayedText;
309
317
  },
310
318
  };
311
319
  }
@@ -422,8 +430,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
422
430
  if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
423
431
  if (onTextDelta || textLeakGuard) {
424
432
  const t = geminiChunkText(parsed);
425
- if (t) relayedText += t;
426
- relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
433
+ relayedText += relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
427
434
  }
428
435
  }
429
436
  }
@@ -443,8 +450,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
443
450
  if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
444
451
  if (onTextDelta || textLeakGuard) {
445
452
  const t = geminiChunkText(parsed);
446
- if (t) relayedText += t;
447
- relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
453
+ relayedText += relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
448
454
  }
449
455
  } catch (cause) {
450
456
  throw geminiStreamCorruptionError(`${label} corrupt SSE tail JSON`, cause);
@@ -675,8 +681,7 @@ export async function consumeGeminiSdkStream(streamResult, {
675
681
  if (!sawFunctionCall && geminiChunkHasFunctionCall(step.value)) sawFunctionCall = true;
676
682
  if (onTextDelta || textLeakGuard) {
677
683
  const t = geminiChunkText(step.value);
678
- if (t) relayedText += t;
679
- relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
684
+ relayedText += relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
680
685
  }
681
686
  }
682
687
  if (idleTimedOut) {
@@ -75,6 +75,11 @@ function geminiRestError(res, text, label) {
75
75
  err.status = res.status;
76
76
  err.httpStatus = res.status;
77
77
  err.headers = res.headers;
78
+ // Initial-response failure: the request was rejected before any sample was
79
+ // produced, so withRetry's typed rules decide: a known transient status
80
+ // retries with bounded backoff (Retry-After honored), anything unknown or
81
+ // deterministic surfaces as an error.
82
+ err.initialResponseError = true;
78
83
  if (detail) {
79
84
  err.error = detail;
80
85
  err.data = payload;
@@ -486,8 +491,11 @@ export class GeminiProvider {
486
491
  try {
487
492
  return await this._doSend(messages, model, tools, sendOpts);
488
493
  } catch (err) {
489
- if (err?.status === 401 || err?.status === 403
490
- || (err?.message && (err.message.includes('401') || err.message.includes('403')))) {
494
+ // Credential reload + reissue requires a TYPED 401: message text is
495
+ // not evidence, and a typed 403 (permission/quota decision) is not
496
+ // fixed by re-reading the key, so it surfaces unchanged.
497
+ const status = Number(err?.status || err?.httpStatus || err?.response?.status || 0);
498
+ if (status === 401) {
491
499
  if (err.liveTextEmitted === true || err.emittedToolCall === true || err.unsafeToRetry === true) {
492
500
  throw err;
493
501
  }
@@ -26,7 +26,6 @@ import { sanitizeModelList } from './model-list-sanitize.mjs';
26
26
  import { makeModelCache } from './model-cache.mjs';
27
27
  import { OpenAICompatProvider } from './openai-compat.mjs';
28
28
  import { createTimeoutSignal } from '../stall-policy.mjs';
29
- import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
30
29
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
31
30
  import { normalizeGrokToolSchemas } from './lib/grok-tool-schema.mjs';
32
31
 
@@ -26,7 +26,6 @@ import { sanitizeModelList } from './model-list-sanitize.mjs';
26
26
  import { makeModelCache } from './model-cache.mjs';
27
27
  import { OpenAICompatProvider } from './openai-compat.mjs';
28
28
  import { createTimeoutSignal } from '../stall-policy.mjs';
29
- import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
30
29
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
31
30
  import { normalizeGrokToolSchemas } from './lib/grok-tool-schema.mjs';
32
31
 
@@ -25,7 +25,6 @@ import { sanitizeModelList } from './model-list-sanitize.mjs';
25
25
  import { makeModelCache } from './model-cache.mjs';
26
26
  import { OpenAICompatProvider } from './openai-compat.mjs';
27
27
  import { createTimeoutSignal } from '../stall-policy.mjs';
28
- import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
29
28
  import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
30
29
  import { normalizeGrokToolSchemas } from './lib/grok-tool-schema.mjs';
31
30
 
@@ -311,10 +310,8 @@ export class GrokOAuthProvider {
311
310
  return await inner._doSend(messages, useModel, grokTools, sendOpts);
312
311
  } catch (err) {
313
312
  // Refresh-and-retry on a server-rejected OAuth session (401/403).
314
- // Resolve the status from the structured field (falling back to the
315
- // shared classifier that derives it from the error text) rather than
316
- // ad-hoc string matching.
317
- populateHttpStatusFromMessage(err);
313
+ // TYPED status only: a rejection whose only evidence is message
314
+ // text is never treated as an auth failure worth re-issuing.
318
315
  const rejectedStatus = Number(err?.httpStatus || err?.status);
319
316
  if (rejectedStatus === 401 || rejectedStatus === 403) {
320
317
  // A stream-level rejection after text/tool dispatch cannot be safely
@@ -0,0 +1,27 @@
1
+ /**
2
+ * anthropic-native-blocks.mjs — single source of truth for Anthropic NATIVE
3
+ * (server-side) tool block types.
4
+ *
5
+ * Anthropic executes these itself and returns BOTH the CALL block
6
+ * (`server_tool_use` / `mcp_tool_use`) and its RESULT block
7
+ * (`web_search_tool_result`, …). They are never dispatched to the agent loop,
8
+ * so they only survive a continuation turn (`stop_reason:'pause_turn'`) when
9
+ * the provider boundary preserves them verbatim, in original block order — a
10
+ * result block is valid ONLY immediately after the call block that produced
11
+ * it. Shared by the SSE parser (streaming) and the non-streaming normalizer.
12
+ */
13
+ export const NATIVE_SERVER_TOOL_CALL_BLOCK_TYPES = new Set(['server_tool_use', 'mcp_tool_use']);
14
+
15
+ export const NATIVE_SERVER_TOOL_RESULT_BLOCK_TYPES = new Set([
16
+ 'web_search_tool_result',
17
+ 'web_fetch_tool_result',
18
+ 'code_execution_tool_result',
19
+ 'bash_code_execution_tool_result',
20
+ 'text_editor_code_execution_tool_result',
21
+ 'mcp_tool_result',
22
+ ]);
23
+
24
+ export function isNativeServerToolBlockType(type) {
25
+ return NATIVE_SERVER_TOOL_CALL_BLOCK_TYPES.has(type)
26
+ || NATIVE_SERVER_TOOL_RESULT_BLOCK_TYPES.has(type);
27
+ }
@@ -1,4 +1,5 @@
1
1
  import { providerNativeToolPrefixCount } from '../../../../../session-runtime/provider-request-tools.mjs';
2
+ import { isNativeServerToolBlockType } from './anthropic-native-blocks.mjs';
2
3
 
3
4
  export const ANTHROPIC_CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' };
4
5
  export const ANTHROPIC_CACHE_TTL_VOLATILE = { type: 'ephemeral' };
@@ -55,6 +56,11 @@ export function clampAnthropicThinkingBudget(value, maxTokens) {
55
56
  return Math.max(1024, Math.min(desired, ceiling));
56
57
  }
57
58
 
59
+ // Assistant blocks that are replayable verbatim alongside native server-tool
60
+ // blocks. `text` is handled separately (empty text blocks are dropped, exactly
61
+ // like parseSSEStream's assistantBlocks list).
62
+ const REPLAYABLE_ASSISTANT_BLOCK_TYPES = new Set(['thinking', 'redacted_thinking', 'tool_use']);
63
+
58
64
  export function normalizeAnthropicNonStreamingResponse(message, fallbackModel = '') {
59
65
  const blocks = Array.isArray(message?.content) ? message.content : [];
60
66
  const text = blocks
@@ -71,6 +77,22 @@ export function normalizeAnthropicNonStreamingResponse(message, fallbackModel =
71
77
  const thinkingBlocks = blocks.filter((block) => (
72
78
  block?.type === 'thinking' || block?.type === 'redacted_thinking'
73
79
  ));
80
+ // Anthropic NATIVE server-tool turns (web search / web fetch / code
81
+ // execution / native MCP) cannot be rebuilt from the flattened
82
+ // text/toolCalls/thinking projections above: `server_tool_use` and its
83
+ // `*_tool_result` are opaque and order-bound (a result block is valid ONLY
84
+ // right after its call block). Keep the ordered raw list so the loop can
85
+ // replay the paused turn verbatim on the `pause_turn` continuation —
86
+ // mirror of parseSSEStream's assistantBlocks, including its
87
+ // absent-for-ordinary-turns rule, so plain text/thinking/tool_use
88
+ // responses keep the existing lowering and never double-replay.
89
+ const nativeAssistantBlocks = blocks.some((block) => isNativeServerToolBlockType(block?.type))
90
+ ? blocks.filter((block) => block && typeof block === 'object' && (
91
+ isNativeServerToolBlockType(block.type)
92
+ || REPLAYABLE_ASSISTANT_BLOCK_TYPES.has(block.type)
93
+ || (block.type === 'text' && typeof block.text === 'string' && block.text.length > 0)
94
+ ))
95
+ : [];
74
96
  const usage = message?.usage || {};
75
97
  const input = Number(usage.input_tokens) || 0;
76
98
  const cacheRead = Number(usage.cache_read_input_tokens) || 0;
@@ -83,6 +105,7 @@ export function normalizeAnthropicNonStreamingResponse(message, fallbackModel =
83
105
  hasThinkingContent: thinkingBlocks.length > 0,
84
106
  contentBlockTypes: blocks.map((block) => block?.type).filter(Boolean),
85
107
  thinkingBlocks: thinkingBlocks.length ? thinkingBlocks : undefined,
108
+ assistantBlocks: nativeAssistantBlocks.length ? nativeAssistantBlocks : undefined,
86
109
  usage: {
87
110
  inputTokens: input,
88
111
  outputTokens: Number(usage.output_tokens) || 0,
@@ -0,0 +1,329 @@
1
+ /**
2
+ * stream-outcome.mjs — canonical provider stream outcome / safety contract.
3
+ *
4
+ * ONE record describes how a provider stream ended, across every transport:
5
+ * OpenAI Responses WS, OpenAI Responses HTTP/SSE, Anthropic API-key SSE,
6
+ * Anthropic OAuth SSE and its non-streaming fallback, and any adapter that
7
+ * only attaches partial state to a thrown error.
8
+ *
9
+ * Historically each consumer (retry classifier, transport fallback,
10
+ * send-with-recovery, ask-session persistence) re-derived safety from a
11
+ * DIFFERENT subset of scattered per-provider flags (liveTextEmitted,
12
+ * emittedText, emittedToolCall, toolCallEmitted, partialToolCall,
13
+ * emittedThinking, emittedReasoning, startedToolCall, partialToolCalls,
14
+ * pendingToolUse, unsafeToRetry, sawCompleted, ...). A provider that set only
15
+ * SOME of them (Anthropic's stall error carries partialContent but neither
16
+ * liveTextEmitted nor unsafeToRetry) silently fell through the gaps: streamed
17
+ * text was dropped from history, or an ambiguous end-of-stream was treated as
18
+ * a clean success / a replayable request.
19
+ *
20
+ * ── Schema (canonical record, frozen) ───────────────────────────────────────
21
+ * version 1
22
+ * provider string|null informational
23
+ * transport 'ws'|'http-sse'|'sse'|'non-streaming'|'unknown'
24
+ * terminalObserved bool provider signalled end-of-turn (response.completed
25
+ * / response.done / message_stop / explicit finish
26
+ * reason / early-settle on a complete tool call)
27
+ * continuation bool stream ended WITHOUT a terminal signal (stall,
28
+ * truncation, abnormal close, EOF mid-message)
29
+ * textEmitted bool non-empty text RELAYED to the client (visible)
30
+ * textObservedChars int assistant text accumulated by the adapter,
31
+ * whether or not it was relayed
32
+ * reasoningEmitted bool thinking/reasoning deltas or blocks exposed
33
+ * toolCallsStarted bool tool input began streaming (args may be partial)
34
+ * toolCallsComplete int fully parsed tool calls
35
+ * toolCallsDispatched int tool calls handed to onToolCall (side effect may
36
+ * already have run)
37
+ * dispatchAmbiguous bool dispatch count was INFERRED, not reported
38
+ * pendingToolInput bool a tool_use/function_call input never completed
39
+ * userAbort bool caller cancelled
40
+ * stallObserved bool watchdog/idle stall produced this outcome
41
+ * truncatedStream bool message_start-without-message_stop class
42
+ *
43
+ * ── Derived invariants ──────────────────────────────────────────────────────
44
+ * terminalObserved clears continuation ONLY when the turn was not declared
45
+ * a same-user-turn continuation. A terminal sample frame may still say
46
+ * `end_turn=false` (Responses) / continuationDeclared: the turn is NOT over,
47
+ * so continuation stays true and successEligible stays false.
48
+ * visibleOutput = textEmitted || reasoningEmitted
49
+ * observedOutput = visibleOutput || textObservedChars > 0
50
+ * || toolCallsComplete > 0 || toolCallsDispatched > 0
51
+ * sideEffectDispatched = toolCallsDispatched > 0
52
+ * replayUnsafe = the ONE architecture-specific deny MixDog adds on top
53
+ * of the Codex retry rules, because it streams UI text
54
+ * and dispatches tools eagerly:
55
+ * visibleOutput || sideEffectDispatched
56
+ * || dispatchAmbiguous || unsafe marker
57
+ * replaySafe = !replayUnsafe && !userAbort
58
+ * successEligible = terminalObserved && !continuation && !pendingToolInput
59
+ *
60
+ * This record does NOT decide retries on its own. Whether a failure is retried
61
+ * at all is the TYPED question owned by retry-classifier.mjs (known transient
62
+ * status / errno / close code → bounded backoff + transport fallback; unknown
63
+ * or untyped → error). `replaySafe` only answers "would re-issuing this turn
64
+ * duplicate something the user already saw or a side effect that already ran".
65
+ * Buffered-but-never-relayed text and a tool input that never completed (and
66
+ * therefore never dispatched) are NOT replay denials; `observedOutput` remains
67
+ * the signal for partial-failure PERSISTENCE, not for replay permission.
68
+ * An absent or ambiguous terminal can never be promoted to success
69
+ * (successEligible=false): an explicit terminal stream event is required.
70
+ *
71
+ * Compatibility: stampStreamOutcome() writes the canonical record AND the
72
+ * historical alias flags (never clearing one that is already true), so every
73
+ * legacy reader keeps working unchanged.
74
+ */
75
+
76
+ export const STREAM_OUTCOME_VERSION = 1
77
+
78
+ export const STREAM_TRANSPORTS = Object.freeze({
79
+ WS: 'ws',
80
+ HTTP_SSE: 'http-sse',
81
+ SSE: 'sse',
82
+ NON_STREAMING: 'non-streaming',
83
+ UNKNOWN: 'unknown',
84
+ })
85
+
86
+ const TRUE = (v) => v === true
87
+ const COUNT = (v) => {
88
+ const n = Number(v)
89
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0
90
+ }
91
+
92
+ function _isAbortLike(source) {
93
+ const name = String(source?.name || '')
94
+ return name === 'AbortError' || name === 'APIUserAbortError' || source?.code === 'ABORT_ERR'
95
+ }
96
+
97
+ /** Raw (pre-derivation) signal read of ONE source object (error or midState). */
98
+ function _signalsOf(source) {
99
+ if (!source || (typeof source !== 'object' && typeof source !== 'function')) return null
100
+
101
+ const partialContent = typeof source.partialContent === 'string' ? source.partialContent : ''
102
+ const partialToolCalls = Array.isArray(source.partialToolCalls) ? source.partialToolCalls.length : 0
103
+ const explicitComplete = COUNT(source.toolCallsComplete)
104
+ const toolCallsComplete = Math.max(partialToolCalls, explicitComplete)
105
+
106
+ const textEmitted = TRUE(source.textEmitted) || TRUE(source.liveTextEmitted) || TRUE(source.emittedText)
107
+ const textObservedChars = Math.max(
108
+ COUNT(source.textObservedChars),
109
+ COUNT(source.emittedTextChars),
110
+ partialContent.trim().length > 0 ? partialContent.length : 0,
111
+ )
112
+ const reasoningEmitted = TRUE(source.reasoningEmitted) || TRUE(source.emittedThinking)
113
+ || TRUE(source.emittedReasoning) || TRUE(source.partialReasoningEmitted)
114
+ // NOTE: `partialHasThinking` (an extended-thinking block EXISTED, possibly
115
+ // empty / signature-only) is deliberately NOT read here: a thinking block
116
+ // the user never saw is not exposed reasoning.
117
+
118
+ // `pendingToolUse` alone is NOT "started": a tool_use whose input JSON never
119
+ // completed was never dispatched, so re-requesting stays idempotent (the
120
+ // documented truncated-stream retry). Only explicit partial-args markers
121
+ // count as exposure.
122
+ const toolCallsStarted = TRUE(source.toolCallsStarted) || TRUE(source.partialToolCall)
123
+ || TRUE(source.startedToolCall) || TRUE(source.partialToolCallStarted)
124
+ || toolCallsComplete > 0
125
+
126
+ const dispatchReported = Object.prototype.hasOwnProperty.call(source, 'toolCallsDispatched')
127
+ const dispatchFlag = TRUE(source.emittedToolCall) || TRUE(source.toolCallEmitted)
128
+ let toolCallsDispatched = dispatchReported ? COUNT(source.toolCallsDispatched) : 0
129
+ let dispatchAmbiguous = false
130
+ if (!dispatchReported) {
131
+ if (dispatchFlag) {
132
+ toolCallsDispatched = Math.max(1, toolCallsComplete)
133
+ dispatchAmbiguous = toolCallsComplete === 0
134
+ } else if (toolCallsComplete > 0) {
135
+ // Fail closed: every streaming adapter dispatches eagerly at
136
+ // content_block_stop / function_call_arguments.done, so a complete tool
137
+ // call with no dispatch report must be assumed to have side effects.
138
+ toolCallsDispatched = toolCallsComplete
139
+ dispatchAmbiguous = true
140
+ }
141
+ }
142
+
143
+ const truncatedStream = TRUE(source.truncatedStream) || source.code === 'TRUNCATED_STREAM'
144
+ const stallObserved = TRUE(source.streamStalled) || source.code === 'ESTREAMSTALL'
145
+ || String(source.name || '') === 'StreamStalledError'
146
+ || String(source.name || '') === 'StreamStalledAbortError'
147
+ || String(source.name || '') === 'AgentStallAbortError'
148
+ || String(source.watchdogAbort || '') !== ''
149
+
150
+ const terminalObserved = TRUE(source.terminalObserved) || TRUE(source.sawCompleted)
151
+ || TRUE(source.completed)
152
+ || (TRUE(source.providerIncomplete) && !!source.finishReason)
153
+ const streamStarted = TRUE(source.sawMessageStart) || TRUE(source.sawResponseCreated)
154
+ || textEmitted || reasoningEmitted || textObservedChars > 0 || toolCallsStarted
155
+ // Protocol distinction: a TERMINAL sample frame can still declare that the
156
+ // same user turn continues (Responses `end_turn=false`). That is a
157
+ // continuation, not a finished turn.
158
+ const declaredContinuation = source.endTurn === false || TRUE(source.continuationDeclared)
159
+
160
+ return {
161
+ provider: source.provider ?? source.providerName ?? null,
162
+ transport: typeof source.transport === 'string' ? source.transport : null,
163
+ terminalObserved,
164
+ continuation: !terminalObserved
165
+ && (truncatedStream || stallObserved || streamStarted || TRUE(source.continuation)),
166
+ declaredContinuation,
167
+ textEmitted,
168
+ textObservedChars,
169
+ reasoningEmitted,
170
+ toolCallsStarted,
171
+ toolCallsComplete,
172
+ toolCallsDispatched,
173
+ // Monotonic across canonical round-trips: an already-recorded ambiguity is
174
+ // never downgraded by a later read that happens to see a dispatch count.
175
+ dispatchAmbiguous: TRUE(source.dispatchAmbiguous) || dispatchAmbiguous,
176
+ pendingToolInput: TRUE(source.pendingToolInput) || TRUE(source.pendingToolUse),
177
+ userAbort: TRUE(source.userAbort) || _isAbortLike(source),
178
+ stallObserved,
179
+ truncatedStream,
180
+ unsafeMarker: TRUE(source.unsafeToRetry),
181
+ }
182
+ }
183
+
184
+ const EMPTY_SIGNALS = {
185
+ provider: null, transport: null,
186
+ terminalObserved: false, continuation: false, declaredContinuation: false,
187
+ textEmitted: false, textObservedChars: 0, reasoningEmitted: false,
188
+ toolCallsStarted: false, toolCallsComplete: 0, toolCallsDispatched: 0,
189
+ dispatchAmbiguous: false, pendingToolInput: false, userAbort: false,
190
+ stallObserved: false, truncatedStream: false, unsafeMarker: false,
191
+ }
192
+
193
+ function _merge(a, b) {
194
+ if (!b) return a
195
+ if (!a) return b
196
+ return {
197
+ provider: a.provider || b.provider || null,
198
+ transport: a.transport || b.transport || null,
199
+ terminalObserved: a.terminalObserved || b.terminalObserved,
200
+ continuation: a.continuation || b.continuation,
201
+ declaredContinuation: a.declaredContinuation || b.declaredContinuation,
202
+ textEmitted: a.textEmitted || b.textEmitted,
203
+ textObservedChars: Math.max(a.textObservedChars, b.textObservedChars),
204
+ reasoningEmitted: a.reasoningEmitted || b.reasoningEmitted,
205
+ toolCallsStarted: a.toolCallsStarted || b.toolCallsStarted,
206
+ toolCallsComplete: Math.max(a.toolCallsComplete, b.toolCallsComplete),
207
+ toolCallsDispatched: Math.max(a.toolCallsDispatched, b.toolCallsDispatched),
208
+ dispatchAmbiguous: a.dispatchAmbiguous || b.dispatchAmbiguous,
209
+ pendingToolInput: a.pendingToolInput || b.pendingToolInput,
210
+ userAbort: a.userAbort || b.userAbort,
211
+ stallObserved: a.stallObserved || b.stallObserved,
212
+ truncatedStream: a.truncatedStream || b.truncatedStream,
213
+ unsafeMarker: a.unsafeMarker || b.unsafeMarker,
214
+ }
215
+ }
216
+
217
+ function _finalize(signals) {
218
+ const s = signals || EMPTY_SIGNALS
219
+ const terminalObserved = s.terminalObserved === true
220
+ const visibleOutput = s.textEmitted || s.reasoningEmitted
221
+ const sideEffectDispatched = s.toolCallsDispatched > 0
222
+ const observedOutput = visibleOutput || s.textObservedChars > 0
223
+ || s.toolCallsComplete > 0 || sideEffectDispatched
224
+ // The architecture-specific deny: a replay would duplicate output the user
225
+ // already saw, or re-run a tool that was already handed off (including the
226
+ // ambiguous case where a complete tool call carries no dispatch report).
227
+ const replayUnsafe = visibleOutput || sideEffectDispatched
228
+ || s.dispatchAmbiguous === true || s.unsafeMarker === true
229
+ const replaySafe = !replayUnsafe && s.userAbort !== true
230
+ // A terminal frame that declared `end_turn=false` keeps the turn open.
231
+ const continuation = s.declaredContinuation === true
232
+ ? true
233
+ : (terminalObserved ? false : s.continuation === true)
234
+ return Object.freeze({
235
+ version: STREAM_OUTCOME_VERSION,
236
+ provider: s.provider || null,
237
+ transport: s.transport || STREAM_TRANSPORTS.UNKNOWN,
238
+ terminalObserved,
239
+ continuation,
240
+ declaredContinuation: s.declaredContinuation === true,
241
+ textEmitted: s.textEmitted === true,
242
+ textObservedChars: s.textObservedChars | 0,
243
+ reasoningEmitted: s.reasoningEmitted === true,
244
+ toolCallsStarted: s.toolCallsStarted === true,
245
+ toolCallsComplete: s.toolCallsComplete | 0,
246
+ toolCallsDispatched: s.toolCallsDispatched | 0,
247
+ dispatchAmbiguous: s.dispatchAmbiguous === true,
248
+ pendingToolInput: s.pendingToolInput === true,
249
+ userAbort: s.userAbort === true,
250
+ stallObserved: s.stallObserved === true,
251
+ truncatedStream: s.truncatedStream === true,
252
+ // Derived verdicts.
253
+ visibleOutput,
254
+ observedOutput,
255
+ sideEffectDispatched,
256
+ replayUnsafe,
257
+ replaySafe,
258
+ successEligible: terminalObserved && !continuation && s.pendingToolInput !== true,
259
+ })
260
+ }
261
+
262
+ /**
263
+ * Normalize any number of sources (thrown error, provider midState, explicit
264
+ * hint object) into the canonical record. Sources are OR/max merged; an
265
+ * already-canonical `source.streamOutcome` is merged in as well.
266
+ */
267
+ export function readStreamOutcome(...sources) {
268
+ let signals = null
269
+ for (const source of sources) {
270
+ if (!source) continue
271
+ const existing = source.streamOutcome
272
+ if (existing && existing.version === STREAM_OUTCOME_VERSION) {
273
+ signals = _merge(signals, _signalsOf(existing))
274
+ }
275
+ signals = _merge(signals, _signalsOf(source))
276
+ }
277
+ return _finalize(signals || EMPTY_SIGNALS)
278
+ }
279
+
280
+ /** Alias kept for callers that read a provider midState directly. */
281
+ export const streamOutcomeFromState = readStreamOutcome
282
+
283
+ /**
284
+ * Attach the canonical record to `target` (usually a thrown error) and mirror
285
+ * it onto the historical alias flags. Aliases are only ever SET, never
286
+ * cleared, so a provider that already stamped one keeps it. Text that was
287
+ * merely observed (buffered, never relayed) does NOT set liveTextEmitted:
288
+ * only relayed text is a visibility/no-replay boundary.
289
+ */
290
+ export function stampStreamOutcome(target, ...sources) {
291
+ if (!target || (typeof target !== 'object' && typeof target !== 'function')) return target
292
+ const outcome = readStreamOutcome(target, ...sources)
293
+ try {
294
+ target.streamOutcome = outcome
295
+ if (outcome.textEmitted) { target.liveTextEmitted = true; target.emittedText = true }
296
+ if (outcome.reasoningEmitted) target.emittedThinking = true
297
+ if (outcome.toolCallsStarted) target.partialToolCall = true
298
+ if (outcome.sideEffectDispatched) { target.emittedToolCall = true; target.toolCallEmitted = true }
299
+ if (outcome.pendingToolInput) target.pendingToolUse = true
300
+ if (outcome.truncatedStream) target.truncatedStream = true
301
+ if (outcome.replayUnsafe) target.unsafeToRetry = true
302
+ } catch { /* frozen/exotic targets keep the returned record only */ }
303
+ return outcome
304
+ }
305
+
306
+ /** True when re-issuing the turn would not duplicate exposure or side effects. */
307
+ export function isReplaySafe(source) {
308
+ return readStreamOutcome(source).replaySafe
309
+ }
310
+
311
+ /** True when there is positive evidence that a replay would duplicate output. */
312
+ export function isReplayUnsafe(source) {
313
+ return readStreamOutcome(source).replayUnsafe
314
+ }
315
+
316
+ /** True when the outcome may be promoted to a normal successful response. */
317
+ export function canPromoteToSuccess(source) {
318
+ return readStreamOutcome(source).successEligible
319
+ }
320
+
321
+ /** True when anything the model produced was observed (visible or buffered). */
322
+ export function hasObservedOutput(source) {
323
+ return readStreamOutcome(source).observedOutput
324
+ }
325
+
326
+ /** True when a tool call was handed off and may already have run. */
327
+ export function hasDispatchedToolCalls(source) {
328
+ return readStreamOutcome(source).sideEffectDispatched
329
+ }
@@ -236,6 +236,18 @@ function normalizeOpenAICodexResetCredits(data, accountId = '') {
236
236
  const availableRows = credits.filter((credit) => credit.status === 'available');
237
237
  if (explicitCount === null && !credits.length) return null;
238
238
  const availableCount = Math.max(0, Math.floor(explicitCount ?? availableRows.length));
239
+ const availableCredits = availableRows
240
+ .map((credit) => ({
241
+ ...(Number.isFinite(credit.expiresAt) && credit.expiresAt > 0
242
+ ? { expiresAt: credit.expiresAt }
243
+ : {}),
244
+ ...(Number.isFinite(credit.grantedAt) && credit.grantedAt > 0
245
+ ? { grantedAt: credit.grantedAt }
246
+ : {}),
247
+ }))
248
+ .sort((left, right) =>
249
+ (left.expiresAt ?? Number.POSITIVE_INFINITY)
250
+ - (right.expiresAt ?? Number.POSITIVE_INFINITY));
239
251
  const expiryCandidates = availableRows
240
252
  .map((credit) => credit.expiresAt)
241
253
  .filter((value) => Number.isFinite(value) && value > 0);
@@ -249,6 +261,7 @@ function normalizeOpenAICodexResetCredits(data, accountId = '') {
249
261
  })).digest('hex')}`;
250
262
  return {
251
263
  availableCount,
264
+ availableCredits,
252
265
  ...(nextExpiresAt ? { nextExpiresAt } : {}),
253
266
  offerRevision,
254
267
  };
@@ -600,7 +613,16 @@ async function fetchOpenAICodexUsage(providerObj) {
600
613
  if (!res.ok) throw new Error(`openai-oauth usage ${res.status}`);
601
614
  const data = await res.json();
602
615
  const usage = normalizeOpenAIWhamUsage(data);
603
- return usage && resetCredits ? { ...usage, resetCredits } : usage;
616
+ // Orca parity: Codex may include the authoritative available count directly
617
+ // in /wham/usage while the detail endpoint is unavailable. Keep that count
618
+ // usable (including a scoped offer revision), then let the dedicated endpoint
619
+ // replace it when richer expiry details arrive.
620
+ const embeddedResetCredits = normalizeOpenAICodexResetCredits(
621
+ data?.rate_limit_reset_credits,
622
+ auth.accountId,
623
+ );
624
+ const resolvedResetCredits = resetCredits || embeddedResetCredits;
625
+ return usage && resolvedResetCredits ? { ...usage, resetCredits: resolvedResetCredits } : usage;
604
626
  }
605
627
 
606
628
  async function fetchAnthropicUsage(providerObj) {