mixdog 0.9.151 → 0.9.152

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 (175) hide show
  1. package/README.md +110 -68
  2. package/package.json +3 -2
  3. package/scripts/tool-stress.mjs +0 -2
  4. package/src/defaults/skills/setup/SKILL.md +18 -27
  5. package/src/headless-exec.mjs +3 -10
  6. package/src/headless-exec.test.mjs +2 -7
  7. package/src/runtime/agent/orchestrator/config.mjs +6 -66
  8. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  9. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  10. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  11. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  12. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  13. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  14. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  16. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  17. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  18. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  19. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
  20. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  21. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  22. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  23. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  25. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  26. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
  28. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
  29. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  30. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
  31. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
  32. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
  33. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  34. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  35. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  36. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
  37. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  38. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
  39. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
  40. package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
  43. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
  44. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
  45. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
  47. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
  48. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
  49. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
  50. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
  51. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  52. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
  53. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
  55. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  57. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  58. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  59. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  60. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  62. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  63. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  64. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  66. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  67. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  68. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  69. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  70. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  71. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  72. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  73. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  74. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  75. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  76. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
  77. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  78. package/src/runtime/browser-bridge/client.mjs +103 -0
  79. package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
  80. package/src/runtime/computer-bridge/client.mjs +101 -0
  81. package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
  82. package/src/runtime/media/store.mjs +18 -8
  83. package/src/runtime/media/store.test.mjs +14 -0
  84. package/src/runtime/memory/index.mjs +1 -1
  85. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  86. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  87. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  88. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  89. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  90. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  91. package/src/runtime/shared/provider-api-key.mjs +1 -0
  92. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  93. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  94. package/src/runtime/shared/skill-document.mjs +86 -0
  95. package/src/runtime/shared/skill-document.test.mjs +81 -0
  96. package/src/runtime/shared/tool-surface.mjs +18 -0
  97. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  98. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  99. package/src/runtime/shared/user-cwd.mjs +33 -14
  100. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  101. package/src/session-runtime/cwd-plugins.mjs +26 -26
  102. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  103. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  104. package/src/session-runtime/global-extensions.mjs +26 -0
  105. package/src/session-runtime/global-extensions.test.mjs +28 -0
  106. package/src/session-runtime/goal-runtime.mjs +721 -0
  107. package/src/session-runtime/goal-runtime.test.mjs +108 -0
  108. package/src/session-runtime/lifecycle-api.mjs +18 -4
  109. package/src/session-runtime/lifecycle-api.test.mjs +21 -0
  110. package/src/session-runtime/mcp-glue.mjs +49 -47
  111. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  112. package/src/session-runtime/plugin-mcp.mjs +96 -22
  113. package/src/session-runtime/prewarm.mjs +1 -10
  114. package/src/session-runtime/resource-api-global.test.mjs +69 -0
  115. package/src/session-runtime/resource-api.mjs +173 -58
  116. package/src/session-runtime/runtime-core.mjs +148 -20
  117. package/src/session-runtime/runtime-tunables.mjs +6 -8
  118. package/src/session-runtime/session-turn-api.mjs +22 -11
  119. package/src/session-runtime/skills-api.mjs +79 -25
  120. package/src/session-runtime/skills-api.test.mjs +99 -0
  121. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  122. package/src/session-runtime/tool-catalog.mjs +4 -13
  123. package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
  124. package/src/session-runtime/tool-surface.mjs +12 -0
  125. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  126. package/src/standalone/agent-tool/notify.mjs +6 -8
  127. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  128. package/src/standalone/daemon.mjs +31 -15
  129. package/src/standalone/hook-bus/config.mjs +1 -0
  130. package/src/standalone/plugin-admin.mjs +17 -0
  131. package/src/standalone/plugin-admin.test.mjs +7 -0
  132. package/src/standalone/provider-admin.mjs +1 -0
  133. package/src/standalone/session-protocol.mjs +5 -0
  134. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  135. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  136. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  137. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  138. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  139. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  140. package/src/standalone/session-runtime-host.mjs +696 -58
  141. package/src/standalone/session-runtime-inline-host.mjs +351 -0
  142. package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
  143. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  144. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  145. package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
  146. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  147. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  148. package/src/standalone/session-runtime-worker.mjs +476 -18
  149. package/src/standalone/session-service.mjs +81 -6
  150. package/src/tui/app/app-view.jsx +1 -0
  151. package/src/tui/app/core-memory-picker.mjs +2 -0
  152. package/src/tui/app/extension-pickers.mjs +1 -1
  153. package/src/tui/app/maintenance-pickers.mjs +3 -3
  154. package/src/tui/app/model-options.mjs +2 -0
  155. package/src/tui/app/model-picker.mjs +14 -4
  156. package/src/tui/app/panel-handoff.test.mjs +311 -0
  157. package/src/tui/app/project-picker.mjs +1 -0
  158. package/src/tui/app/route-pickers.mjs +21 -6
  159. package/src/tui/app/settings-picker.mjs +26 -3
  160. package/src/tui/app/slash-commands.mjs +1 -0
  161. package/src/tui/app/slash-dispatch.mjs +73 -22
  162. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  163. package/src/tui/components/Picker.jsx +10 -1
  164. package/src/tui/dist/index.mjs +163 -44
  165. package/src/tui/session/context-state.mjs +1 -0
  166. package/src/tui/session/goal-continuation.mjs +110 -0
  167. package/src/tui/session/goal-continuation.test.mjs +84 -0
  168. package/src/tui/session/queue-helpers.mjs +5 -2
  169. package/src/tui/session/session-action-surface.test.mjs +108 -0
  170. package/src/tui/session/session-api-ext.mjs +39 -8
  171. package/src/tui/session/session-api.mjs +58 -0
  172. package/src/tui/session/session-flow.mjs +6 -0
  173. package/src/tui/session/turn.mjs +1 -0
  174. package/src/tui/session-local.mjs +15 -1
  175. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -64,6 +64,7 @@ import {
64
64
  writeXaiResponsesCacheTrace,
65
65
  } from './openai-compat-xai.mjs';
66
66
  import { envFlag as _envFlag } from './lib/env-utils.mjs';
67
+ import { createProviderReplay } from './lib/provider-replay.mjs';
67
68
 
68
69
  const requireOpenAI = createRequire(import.meta.url);
69
70
  let _OpenAI = null;
@@ -168,10 +169,33 @@ function normalizeReasoningEffort(value, allowed) {
168
169
  return allowed.includes(effort) ? effort : null;
169
170
  }
170
171
 
172
+ export function compatReportedCostUsd(providerName, usage) {
173
+ if (providerName === 'openrouter') {
174
+ const cost = Number(usage?.cost);
175
+ return Number.isFinite(cost) && cost >= 0 ? cost : undefined;
176
+ }
177
+ if (providerName === 'xai') {
178
+ const ticks = Number(usage?.cost_in_usd_ticks);
179
+ return Number.isFinite(ticks) && ticks >= 0
180
+ ? Number((ticks * 1e-10).toFixed(8))
181
+ : undefined;
182
+ }
183
+ return undefined;
184
+ }
185
+
171
186
  // Keep provider extensions isolated: fields accepted by one OpenAI-compatible
172
187
  // backend are frequently rejected by another even when the core Chat schema is
173
188
  // shared.
174
189
  export function applyCompatProviderChatOptions(params, providerName, opts = {}, config = {}, modelInfo = null) {
190
+ if (providerName === 'openrouter') {
191
+ const effort = normalizeReasoningEffort(
192
+ opts.openRouterReasoningEffort ?? opts.effort ?? config?.reasoningEffort,
193
+ ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'],
194
+ );
195
+ if (effort === 'none') params.reasoning = { enabled: false };
196
+ else if (effort) params.reasoning = { enabled: true, effort };
197
+ return params;
198
+ }
175
199
  if (providerName === 'xai') {
176
200
  const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
177
201
  ?? opts.effort
@@ -594,6 +618,9 @@ export class OpenAICompatProvider {
594
618
  const reasoningContent = (capturesReasoningContent && typeof assembled.reasoningContent === 'string')
595
619
  ? assembled.reasoningContent
596
620
  : null;
621
+ const reasoningDetails = this.name === 'openrouter' && Array.isArray(choice?.message?.reasoning_details)
622
+ ? choice.message.reasoning_details
623
+ : null;
597
624
  return {
598
625
  content: assembled.content || '',
599
626
  // Streamed chunks can omit `model`; fall back to the requested
@@ -609,6 +636,9 @@ export class OpenAICompatProvider {
609
636
  // instead of silently treating a truncated answer as complete.
610
637
  ...(stopReason === 'length' && (assembled.content || '').length > 0 ? { truncated: true } : {}),
611
638
  ...(reasoningContent ? { reasoningContent } : {}),
639
+ ...(reasoningDetails?.length ? {
640
+ providerMetadata: { openrouter: { reasoning_details: reasoningDetails } },
641
+ } : {}),
612
642
  usage: response.usage ? (() => {
613
643
  const input = response.usage.prompt_tokens ?? response.usage.input_tokens ?? 0;
614
644
  const cached = extractCompatCachedTokens(response.usage);
@@ -616,10 +646,7 @@ export class OpenAICompatProvider {
616
646
  // (1 tick = $1e-10, per docs.x.ai). Surface it as costUsd so the
617
647
  // session manager skips the catalog-rate fallback and records the
618
648
  // provider-billed value verbatim.
619
- const ticks = response.usage.cost_in_usd_ticks;
620
- const costUsd = typeof ticks === 'number' && ticks >= 0
621
- ? Number((ticks * 1e-10).toFixed(8))
622
- : undefined;
649
+ const costUsd = compatReportedCostUsd(this.name, response.usage);
623
650
  return {
624
651
  inputTokens: input,
625
652
  outputTokens: response.usage.completion_tokens ?? response.usage.output_tokens ?? 0,
@@ -884,6 +911,7 @@ export class OpenAICompatProvider {
884
911
  // any rejection as a provider-side drop.)
885
912
  const nextPreviousResponseId = response.id;
886
913
  const encryptedReasoningItems = encryptedXaiReasoningItems(response.output);
914
+ const providerReplay = createProviderReplay('xai-responses', response.output);
887
915
  const encryptedReasoningHistory = [
888
916
  ...(Array.isArray(opts.providerState?.xaiResponses?.encryptedReasoningHistory)
889
917
  ? opts.providerState.xaiResponses.encryptedReasoningHistory
@@ -904,6 +932,7 @@ export class OpenAICompatProvider {
904
932
  ...(streamed.stopReason === 'length' && (streamed.content || '').length > 0 ? { truncated: true } : {}),
905
933
  citations: searchSources.citations.length ? searchSources.citations : undefined,
906
934
  webSearchCalls: searchSources.webSearchCalls.length ? searchSources.webSearchCalls : undefined,
935
+ providerReplay,
907
936
  providerState: {
908
937
  ...(opts.providerState || {}),
909
938
  xaiResponses: {
@@ -1022,6 +1051,7 @@ export class OpenAICompatProvider {
1022
1051
  // Same rationale as the HTTP path above: `length` stop keeps the chain.
1023
1052
  const nextPreviousResponseId = responseId;
1024
1053
  const encryptedReasoningItems = encryptedXaiReasoningItems(result.responseItems);
1054
+ const providerReplay = createProviderReplay('xai-responses', result.responseItems);
1025
1055
  const encryptedReasoningHistory = [
1026
1056
  ...(Array.isArray(opts.providerState?.xaiResponses?.encryptedReasoningHistory)
1027
1057
  ? opts.providerState.xaiResponses.encryptedReasoningHistory
@@ -1080,6 +1110,7 @@ export class OpenAICompatProvider {
1080
1110
  // P1 audit fix: same truncated signal as the HTTP path (see
1081
1111
  // _doSendXaiResponses above) for the WebSocket transport.
1082
1112
  ...(result.stopReason === 'length' && (result.content || '').length > 0 ? { truncated: true } : {}),
1113
+ providerReplay,
1083
1114
  providerState: {
1084
1115
  ...(opts.providerState || {}),
1085
1116
  xaiResponses: {
@@ -38,6 +38,7 @@ import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './ant
38
38
  import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
39
39
  import { CODEX_OAUTH_ORIGINATOR, CODEX_RESPONSES_URL, _displayCodexModel } from './openai-oauth.mjs';
40
40
  import { createActiveToolItemTracker } from './tool-stream-state.mjs';
41
+ import { createProviderReplay } from './lib/provider-replay.mjs';
41
42
  import { parseProviderJsonBatch } from './stream-json-pool.mjs';
42
43
  import { envFlag as _envFlag } from './lib/env-utils.mjs';
43
44
  export { envPositiveInt as _envPositiveInt } from './lib/env-utils.mjs';
@@ -504,6 +505,8 @@ export async function sendViaHttpSse({
504
505
  const clearActiveToolItem = _toolTracker.clear;
505
506
  let _toolInFlight = false;
506
507
  const reasoningItems = [];
508
+ const responseItems = [];
509
+ const responseItemKeys = new Set();
507
510
  const citations = [];
508
511
  const citationKeys = new Set();
509
512
  const webSearchCalls = [];
@@ -663,6 +666,16 @@ export async function sendViaHttpSse({
663
666
  });
664
667
  }
665
668
  };
669
+ const pushResponseItem = (item) => {
670
+ if (!item || typeof item !== 'object') return;
671
+ let fallbackKey = '';
672
+ try { fallbackKey = JSON.stringify(item); } catch {}
673
+ const key = `${item.type || 'unknown'}:${item.id || item.call_id || fallbackKey}`;
674
+ if (responseItemKeys.has(key)) return;
675
+ responseItemKeys.add(key);
676
+ try { responseItems.push(structuredClone(item)); }
677
+ catch { responseItems.push({ ...item }); }
678
+ };
666
679
  const pushToolSearchCall = (item) => {
667
680
  if (!item || item.type !== 'tool_search_call') return;
668
681
  const callId = item.call_id || item.id || '';
@@ -800,6 +813,7 @@ export async function sendViaHttpSse({
800
813
  break;
801
814
  case 'response.output_item.done': {
802
815
  const item = event.item || {};
816
+ pushResponseItem(item);
803
817
  pushReasoningItem(item);
804
818
  pushWebSearchCall(item);
805
819
  if (item.type === 'function_call') {
@@ -852,6 +866,7 @@ export async function sendViaHttpSse({
852
866
  }
853
867
  let reportedBundleProgress = false;
854
868
  for (const item of resp.output || []) {
869
+ pushResponseItem(item);
855
870
  if (item.type === 'message') {
856
871
  for (const part of item.content || []) {
857
872
  if (!content && part.type === 'output_text') {
@@ -1060,6 +1075,11 @@ export async function sendViaHttpSse({
1060
1075
  // trailing text is never lost (streamed-text path).
1061
1076
  flushLeak();
1062
1077
  } catch (err) {
1078
+ if (err && !err.partialProviderReplay) {
1079
+ try {
1080
+ err.partialProviderReplay = createProviderReplay('openai-responses', responseItems);
1081
+ } catch {}
1082
+ }
1063
1083
  // Live-text invariant: once a non-empty chunk has been relayed it
1064
1084
  // cannot be withdrawn — flag the error so no upstream layer retries.
1065
1085
  if (emittedText && err) { try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {} }
@@ -1100,6 +1120,7 @@ export async function sendViaHttpSse({
1100
1120
  try {
1101
1121
  err.partialContent = content;
1102
1122
  err.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
1123
+ err.partialProviderReplay = createProviderReplay('openai-responses', responseItems);
1103
1124
  err.partialModel = model || undefined;
1104
1125
  err.pendingToolUse = pendingCalls.size > 0 || activeToolItems.size > 0 || _toolInFlight === true;
1105
1126
  } catch { /* best-effort enrichment */ }
@@ -1141,6 +1162,7 @@ export async function sendViaHttpSse({
1141
1162
  content,
1142
1163
  model: liveModel,
1143
1164
  reasoningItems: reasoningItems.length ? reasoningItems : undefined,
1165
+ providerReplay: createProviderReplay('openai-responses', responseItems),
1144
1166
  toolCalls: _returnedToolCalls,
1145
1167
  citations: citations.length ? citations : undefined,
1146
1168
  webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
@@ -50,7 +50,6 @@ import {
50
50
  acquireWebSocket,
51
51
  releaseWebSocket,
52
52
  _sendFrame,
53
- _closeAllPooledSockets,
54
53
  drainOpenaiWsPool,
55
54
  } from './openai-ws-pool.mjs';
56
55
  import {
@@ -74,7 +73,6 @@ import { envPositiveInt } from './lib/env-utils.mjs';
74
73
  // (drainOpenaiWsPool), scripts/provider-toolcall-test.mjs (parseToolSearchArgs,
75
74
  // _logicalResponseItemMatch, _streamResponse) and other external callers.
76
75
  export {
77
- _closeAllPooledSockets,
78
76
  drainOpenaiWsPool,
79
77
  _logicalResponseItemMatch,
80
78
  parseToolSearchArgs,
@@ -17,6 +17,7 @@ import { sanitizeModelList } from './model-list-sanitize.mjs';
17
17
  import { writeJsonAtomicSync, withFileLock } from '../../../shared/atomic-file.mjs';
18
18
  import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
19
19
  import { makeModelCache } from './model-cache.mjs';
20
+ import { providerReplayItems } from './lib/provider-replay.mjs';
20
21
 
21
22
  import { sendViaWebSocket } from './openai-oauth-ws.mjs';
22
23
  import { _combineUsageWithWarmup } from './openai-ws-events.mjs';
@@ -175,6 +176,18 @@ export function convertMessagesToResponsesInput(messages, opts = {}) {
175
176
  continue;
176
177
  }
177
178
  flushToolMedia();
179
+ const orderedReplay = replayEncryptedReasoning && m.role === 'assistant'
180
+ ? providerReplayItems(m, 'openai-responses')
181
+ : undefined;
182
+ if (orderedReplay?.length) {
183
+ for (const item of orderedReplay) {
184
+ if (item?.type === 'custom_tool_call' && item.call_id) {
185
+ customToolCallNameById.set(item.call_id, item.name || '');
186
+ }
187
+ out.push(item);
188
+ }
189
+ continue;
190
+ }
178
191
  pushReasoningItems(m, 'before');
179
192
  if (m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length) {
180
193
  // Default path deliberately omits reasoning replay: openai-oauth
@@ -184,7 +184,7 @@ function _getPoolArr(poolKey) {
184
184
  // cap-overflow ephemeral. They were invisible to per-session close and to the
185
185
  // global drain, so a wedged one survived both. Tracking them here keeps the
186
186
  // reuse map unchanged (nothing can select them) while making every owned socket
187
- // reachable by closeOpenaiWsPoolForSession/_closeAllPooledSockets.
187
+ // reachable by closeOpenaiWsPoolForSession/drainOpenaiWsPool.
188
188
  const _unpooledSockets = new Set();
189
189
 
190
190
  function _trackUnpooled(entry, poolKey) {
@@ -883,16 +883,14 @@ export function _setOpenSocketForTest(fn) {
883
883
  _openSocketImpl = typeof fn === 'function' ? fn : _openSocket;
884
884
  }
885
885
 
886
- // Drain-complete fence — set true once _closeAllPooledSockets runs so any
886
+ // Drain-complete fence — set true once drainOpenaiWsPool runs so any
887
887
  // in-flight acquire that resumes after drain throws instead of pushing a
888
888
  // fresh socket into the cleared pool. Single-set, process-lifetime invariant.
889
889
  let _drainComplete = false;
890
890
 
891
891
  // Drain hook — self-registered exit drain.
892
892
  // Force-closes pooled sockets and fences subsequent acquires.
893
- // `drainOpenaiWsPool` alias matches the registry's `drain*` naming convention;
894
- // `_closeAllPooledSockets` kept for backward compat with existing call sites.
895
- export function _closeAllPooledSockets(reason = 'shutdown', { immediate = false } = {}) {
893
+ export function drainOpenaiWsPool(reason = 'shutdown', { immediate = false } = {}) {
896
894
  _drainComplete = true;
897
895
  for (const arr of _wsPool.values()) {
898
896
  // _shutdownEntry tears down per-entry timers before the map is dropped
@@ -909,7 +907,6 @@ export function _closeAllPooledSockets(reason = 'shutdown', { immediate = false
909
907
  _unpooledSockets.clear();
910
908
  _wsPool.clear();
911
909
  }
912
- export const drainOpenaiWsPool = _closeAllPooledSockets;
913
910
  // Session-close / drain hooks are shared across provider transports (cursor-wire
914
911
  // registers the same globals), so chain whatever is already installed instead of
915
912
  // replacing it — import order must not decide which transport gets torn down.
@@ -925,4 +922,4 @@ globalThis.__mixdogDrainProviderConnections = (reason) => {
925
922
  };
926
923
  // 'exit' hands the listener an exit CODE, never a reason string, and no timer
927
924
  // can fire after it: terminate immediately instead of scheduling.
928
- process.on('exit', () => _closeAllPooledSockets('process-exit', { immediate: true }));
925
+ process.on('exit', () => drainOpenaiWsPool('process-exit', { immediate: true }));
@@ -33,6 +33,7 @@ import {
33
33
  streamStalledError,
34
34
  } from '../stall-policy.mjs';
35
35
  import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
36
+ import { createProviderReplay } from './lib/provider-replay.mjs';
36
37
  import { _wsErrLabel, WS_MAX_INCOMING_FRAME_BYTES } from './openai-ws-pool.mjs';
37
38
  import {
38
39
  _sansInput,
@@ -675,6 +676,10 @@ export async function _streamResponse({
675
676
  try {
676
677
  err.partialContent = content;
677
678
  err.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
679
+ err.partialProviderReplay = createProviderReplay(
680
+ traceProvider === 'xai' ? 'xai-responses' : 'openai-responses',
681
+ responseItemsAdded,
682
+ );
678
683
  err.pendingToolUse = pendingCalls.size > 0
679
684
  || activeToolItems.size > 0
680
685
  || _toolInFlight === true;
@@ -794,6 +799,10 @@ export async function _streamResponse({
794
799
  model,
795
800
  reasoningItems: reasoningItems.length ? reasoningItems : undefined,
796
801
  responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
802
+ providerReplay: createProviderReplay(
803
+ traceProvider === 'xai' ? 'xai-responses' : 'openai-responses',
804
+ responseItemsAdded,
805
+ ),
797
806
  // Dedupe by name+args (Fix 2, array side) so an identical
798
807
  // synthetic-leaked + native pair can't run the tool twice.
799
808
  toolCalls: toolCalls.length ? dedupeToolCallList(toolCalls) : undefined,
@@ -0,0 +1,235 @@
1
+ import assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+
4
+ import {
5
+ createProviderReplay,
6
+ providerReplayItems,
7
+ } from './lib/provider-replay.mjs';
8
+ import { toAnthropicMessages } from './lib/anthropic-request-utils.mjs';
9
+ import { parseSSEStream as parseAnthropicSSEStream } from './anthropic-sse.mjs';
10
+ import { toGeminiContents } from './gemini-schema.mjs';
11
+ import { convertMessagesToResponsesInput } from './openai-responses-payload.mjs';
12
+ import { toOpenAIMessages, toXaiResponsesInput } from './openai-compat-wire.mjs';
13
+ import { estimateMessagesTokens } from '../session/context-utils.mjs';
14
+ import { _sessionForDisk } from '../session/store/serialize.mjs';
15
+
16
+ function interleavedAnthropicBlocks() {
17
+ const blocks = [{ type: 'thinking', thinking: '', signature: 'sig-a' }];
18
+ for (let index = 1; index <= 10; index += 1) {
19
+ blocks.push({
20
+ type: 'tool_use',
21
+ id: `toolu_${index}`,
22
+ name: 'read',
23
+ input: { index },
24
+ });
25
+ }
26
+ blocks.push({ type: 'thinking', thinking: '', signature: 'sig-b' });
27
+ blocks.push({
28
+ type: 'tool_use',
29
+ id: 'toolu_11',
30
+ name: 'read',
31
+ input: { index: 11 },
32
+ });
33
+ return blocks;
34
+ }
35
+
36
+ function interleavedResponsesItems() {
37
+ const items = [{
38
+ type: 'reasoning',
39
+ id: 'rs_1',
40
+ encrypted_content: 'cipher-a',
41
+ summary: [],
42
+ }];
43
+ for (let index = 1; index <= 10; index += 1) {
44
+ items.push({
45
+ type: 'function_call',
46
+ call_id: `call_${index}`,
47
+ name: 'read',
48
+ arguments: JSON.stringify({ index }),
49
+ });
50
+ }
51
+ items.push({
52
+ type: 'reasoning',
53
+ id: 'rs_2',
54
+ encrypted_content: 'cipher-b',
55
+ summary: [],
56
+ });
57
+ items.push({
58
+ type: 'function_call',
59
+ call_id: 'call_11',
60
+ name: 'read',
61
+ arguments: '{"index":11}',
62
+ });
63
+ return items;
64
+ }
65
+
66
+ function anthropicSseResponse(events) {
67
+ const body = events
68
+ .map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`)
69
+ .join('');
70
+ return new Response(body, { headers: { 'content-type': 'text/event-stream' } });
71
+ }
72
+
73
+ test('provider replay is detached and scoped to its originating provider', () => {
74
+ const source = [{ type: 'thinking', thinking: 'plan', signature: 'sig' }];
75
+ const replay = createProviderReplay('anthropic', source);
76
+ source[0].signature = 'mutated';
77
+
78
+ assert.equal(replay.items[0].signature, 'sig');
79
+ assert.deepEqual(providerReplayItems({ providerReplay: replay }, 'anthropic'), replay.items);
80
+ assert.equal(providerReplayItems({ providerReplay: replay }, 'gemini'), undefined);
81
+ });
82
+
83
+ test('Anthropic keeps interleaved thinking at content[11]', () => {
84
+ const blocks = interleavedAnthropicBlocks();
85
+ const message = {
86
+ role: 'assistant',
87
+ content: '',
88
+ toolCalls: blocks
89
+ .filter((block) => block.type === 'tool_use')
90
+ .map((block) => ({
91
+ id: block.id,
92
+ name: block.name,
93
+ arguments: block.input,
94
+ })),
95
+ providerReplay: createProviderReplay('anthropic', blocks),
96
+ };
97
+ const lowered = toAnthropicMessages([message]);
98
+
99
+ assert.deepEqual(lowered[0].content, blocks);
100
+ assert.equal(lowered[0].content[11].type, 'thinking');
101
+ assert.equal(lowered[0].content[11].signature, 'sig-b');
102
+ });
103
+
104
+ test('Anthropic SSE parse to next-request replay keeps content[11] unchanged', async () => {
105
+ const events = [
106
+ { type: 'message_start', message: { model: 'claude-test', usage: { input_tokens: 1 } } },
107
+ { type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: '' } },
108
+ { type: 'content_block_delta', index: 0, delta: { type: 'signature_delta', signature: 'sig-a' } },
109
+ { type: 'content_block_stop', index: 0 },
110
+ ];
111
+ for (let index = 1; index <= 10; index += 1) {
112
+ events.push(
113
+ {
114
+ type: 'content_block_start',
115
+ index,
116
+ content_block: { type: 'tool_use', id: `toolu_${index}`, name: 'read' },
117
+ },
118
+ {
119
+ type: 'content_block_delta',
120
+ index,
121
+ delta: { type: 'input_json_delta', partial_json: JSON.stringify({ index }) },
122
+ },
123
+ { type: 'content_block_stop', index },
124
+ );
125
+ }
126
+ events.push(
127
+ { type: 'content_block_start', index: 11, content_block: { type: 'thinking', thinking: '' } },
128
+ { type: 'content_block_delta', index: 11, delta: { type: 'signature_delta', signature: 'sig-b' } },
129
+ { type: 'content_block_stop', index: 11 },
130
+ {
131
+ type: 'content_block_start',
132
+ index: 12,
133
+ content_block: { type: 'tool_use', id: 'toolu_11', name: 'read' },
134
+ },
135
+ {
136
+ type: 'content_block_delta',
137
+ index: 12,
138
+ delta: { type: 'input_json_delta', partial_json: '{"index":11}' },
139
+ },
140
+ { type: 'content_block_stop', index: 12 },
141
+ { type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
142
+ { type: 'message_stop' },
143
+ );
144
+ const parsed = await parseAnthropicSSEStream(
145
+ anthropicSseResponse(events),
146
+ null,
147
+ () => {},
148
+ () => {},
149
+ () => {},
150
+ {},
151
+ null,
152
+ new Set(['read']),
153
+ );
154
+ const lowered = toAnthropicMessages([{
155
+ role: 'assistant',
156
+ content: parsed.content || '',
157
+ toolCalls: parsed.toolCalls,
158
+ providerReplay: parsed.providerReplay,
159
+ }]);
160
+
161
+ assert.equal(parsed.providerReplay.items[11].type, 'thinking');
162
+ assert.deepEqual(lowered[0].content, parsed.providerReplay.items);
163
+ });
164
+
165
+ test('Gemini keeps signed thought Parts interleaved with function calls', () => {
166
+ const parts = [{ text: 'plan-a', thought: true, thoughtSignature: 'sig-a' }];
167
+ for (let index = 1; index <= 10; index += 1) {
168
+ parts.push({ functionCall: { id: `call_${index}`, name: 'read', args: { index } } });
169
+ }
170
+ parts.push({ text: 'plan-b', thought: true, thoughtSignature: 'sig-b' });
171
+ parts.push({ functionCall: { id: 'call_11', name: 'read', args: { index: 11 } } });
172
+ const contents = toGeminiContents([{
173
+ role: 'assistant',
174
+ content: '',
175
+ toolCalls: [],
176
+ providerReplay: createProviderReplay('gemini', parts),
177
+ }], 'gemini-3-pro');
178
+
179
+ assert.deepEqual(contents[0].parts, parts);
180
+ assert.equal(contents[0].parts[11].thoughtSignature, 'sig-b');
181
+ });
182
+
183
+ test('OpenAI Responses recovery keeps reasoning between function calls', () => {
184
+ const items = interleavedResponsesItems();
185
+ const input = convertMessagesToResponsesInput([{
186
+ role: 'assistant',
187
+ content: '',
188
+ toolCalls: [],
189
+ providerReplay: createProviderReplay('openai-responses', items),
190
+ }], { replayEncryptedReasoning: true });
191
+
192
+ assert.deepEqual(input, items);
193
+ assert.equal(input[11].type, 'reasoning');
194
+ assert.equal(input[11].id, 'rs_2');
195
+ });
196
+
197
+ test('xAI stateless recovery keeps encrypted reasoning output order', () => {
198
+ const items = interleavedResponsesItems();
199
+ const result = toXaiResponsesInput([{
200
+ role: 'assistant',
201
+ content: '',
202
+ toolCalls: [],
203
+ providerReplay: createProviderReplay('xai-responses', items),
204
+ }], null);
205
+
206
+ assert.deepEqual(result.input, items);
207
+ assert.equal(result.input[11].type, 'reasoning');
208
+ });
209
+
210
+ test('OpenRouter reasoning_details remains an unmodified assistant field', () => {
211
+ const reasoningDetails = [
212
+ { type: 'reasoning.text', text: 'plan', signature: 'opaque' },
213
+ { type: 'reasoning.encrypted', data: 'ciphertext' },
214
+ ];
215
+ const wire = toOpenAIMessages([{
216
+ role: 'assistant',
217
+ content: '',
218
+ toolCalls: [{ id: 'call_1', name: 'read', arguments: { path: 'a' } }],
219
+ providerMetadata: { openrouter: { reasoning_details: reasoningDetails } },
220
+ }], 'openrouter');
221
+
222
+ assert.equal(wire[0].reasoning_details, reasoningDetails);
223
+ });
224
+
225
+ test('provider replay survives disk projection and contributes to context size', () => {
226
+ const providerReplay = createProviderReplay('openai-responses', interleavedResponsesItems());
227
+ const message = { role: 'assistant', content: '', providerReplay };
228
+ const stored = _sessionForDisk({ id: 's', messages: [message] });
229
+
230
+ assert.deepEqual(stored.messages[0].providerReplay, providerReplay);
231
+ assert.ok(
232
+ estimateMessagesTokens([message])
233
+ > estimateMessagesTokens([{ role: 'assistant', content: '' }]),
234
+ );
235
+ });