mixdog 0.9.50 → 0.9.52

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 (134) hide show
  1. package/package.json +5 -3
  2. package/scripts/abort-recovery-test.mjs +17 -1
  3. package/scripts/agent-model-liveness-test.mjs +79 -2
  4. package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
  5. package/scripts/anthropic-transport-policy-test.mjs +466 -0
  6. package/scripts/atomic-lock-tryonce-test.mjs +60 -1
  7. package/scripts/build-tui.mjs +13 -1
  8. package/scripts/channel-daemon-smoke.mjs +630 -10
  9. package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
  10. package/scripts/code-graph-disk-hit-test.mjs +11 -0
  11. package/scripts/code-graph-root-federation-test.mjs +273 -0
  12. package/scripts/compact-pressure-test.mjs +55 -1
  13. package/scripts/compact-smoke.mjs +80 -0
  14. package/scripts/context-mcp-metering-test.mjs +1350 -19
  15. package/scripts/deferred-tool-loading-test.mjs +17 -0
  16. package/scripts/gemini-provider-test.mjs +1053 -0
  17. package/scripts/hook-bus-test.mjs +23 -0
  18. package/scripts/internal-tools-normalization-test.mjs +10 -0
  19. package/scripts/interrupted-turn-history-test.mjs +371 -0
  20. package/scripts/lifecycle-api-test.mjs +76 -0
  21. package/scripts/max-output-recovery-test.mjs +55 -0
  22. package/scripts/mcp-grace-deferred-test.mjs +89 -13
  23. package/scripts/memory-pg-recovery-test.mjs +59 -0
  24. package/scripts/openai-oauth-ws-1006-retry-test.mjs +391 -4
  25. package/scripts/process-lifecycle-test.mjs +389 -0
  26. package/scripts/provider-admission-scheduler-test.mjs +582 -0
  27. package/scripts/provider-contract-test.mjs +268 -0
  28. package/scripts/provider-toolcall-test.mjs +520 -3
  29. package/scripts/reactive-compact-persist-smoke.mjs +59 -0
  30. package/scripts/resource-admission-test.mjs +789 -0
  31. package/scripts/session-bench-cache-break-test.mjs +102 -0
  32. package/scripts/session-bench.mjs +101 -42
  33. package/scripts/shell-failure-diagnostics-test.mjs +73 -4
  34. package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
  35. package/scripts/smoke-loop-failure-summary-test.mjs +38 -0
  36. package/scripts/smoke-loop-failure-summary.mjs +16 -0
  37. package/scripts/smoke-loop.mjs +2 -7
  38. package/scripts/steering-drain-buckets-test.mjs +18 -0
  39. package/scripts/tool-failures.mjs +15 -1
  40. package/scripts/toolcall-args-test.mjs +14 -6
  41. package/scripts/tui-transcript-perf-test.mjs +43 -7
  42. package/scripts/web-fetch-routing-test.mjs +158 -0
  43. package/src/cli.mjs +15 -2
  44. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
  45. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
  46. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -0
  47. package/src/runtime/agent/orchestrator/internal-tools.mjs +2 -0
  48. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
  49. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
  50. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
  51. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
  52. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
  53. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
  54. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
  55. package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
  56. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
  57. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
  60. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
  61. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
  62. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
  63. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +144 -51
  64. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
  65. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +74 -1
  66. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +111 -6
  67. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
  68. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
  69. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
  70. package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
  71. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
  72. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +127 -55
  73. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
  74. package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
  75. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +38 -0
  76. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -0
  77. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
  78. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
  79. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
  80. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
  81. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
  83. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
  84. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +5 -1
  85. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
  86. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +24 -4
  87. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
  88. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
  89. package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
  90. package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
  91. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
  92. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
  93. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
  94. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
  95. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
  96. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
  97. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
  98. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
  99. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
  100. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
  101. package/src/runtime/memory/index.mjs +22 -4
  102. package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
  103. package/src/runtime/memory/lib/pg/process.mjs +91 -47
  104. package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
  105. package/src/runtime/search/index.mjs +41 -0
  106. package/src/runtime/search/lib/http-fetch.mjs +154 -0
  107. package/src/runtime/search/tool-defs.mjs +23 -0
  108. package/src/runtime/shared/atomic-file.mjs +28 -150
  109. package/src/runtime/shared/process-lifecycle.mjs +363 -0
  110. package/src/runtime/shared/process-shutdown.mjs +27 -4
  111. package/src/runtime/shared/resource-admission.mjs +359 -0
  112. package/src/runtime/shared/staged-child-result.mjs +19 -0
  113. package/src/session-runtime/context-status.mjs +38 -27
  114. package/src/session-runtime/lifecycle-api.mjs +26 -6
  115. package/src/session-runtime/provider-request-tools.mjs +72 -0
  116. package/src/session-runtime/runtime-core.mjs +43 -18
  117. package/src/session-runtime/session-turn-api.mjs +5 -4
  118. package/src/session-runtime/tool-catalog.mjs +375 -15
  119. package/src/standalone/agent-tool.mjs +17 -38
  120. package/src/standalone/channel-daemon-client.mjs +200 -38
  121. package/src/standalone/channel-daemon-transport.mjs +136 -9
  122. package/src/standalone/channel-worker.mjs +79 -12
  123. package/src/standalone/hook-bus/handlers.mjs +4 -0
  124. package/src/standalone/hook-bus/rules.mjs +7 -1
  125. package/src/standalone/hook-bus.mjs +10 -2
  126. package/src/tui/App.jsx +17 -11
  127. package/src/tui/app/live-spinner-visibility.mjs +20 -0
  128. package/src/tui/dist/index.mjs +101 -281
  129. package/src/tui/engine/session-api-ext.mjs +13 -2
  130. package/src/tui/engine/session-api.mjs +1 -1
  131. package/src/tui/engine/turn.mjs +10 -6
  132. package/src/tui/engine.mjs +13 -4
  133. package/src/tui/index.jsx +4 -1
  134. package/src/tui/lib/voice-setup.mjs +16 -0
@@ -10,6 +10,9 @@
10
10
  // documented inline per provider block below.
11
11
  import test from 'node:test';
12
12
  import assert from 'node:assert/strict';
13
+ import { readFileSync } from 'node:fs';
14
+ import { parse } from 'acorn';
15
+ import { analyze } from 'eslint-scope';
13
16
 
14
17
  import {
15
18
  OpenAICompatProvider,
@@ -37,6 +40,7 @@ import {
37
40
  import {
38
41
  _cacheObservationForTest,
39
42
  _cacheContinuityResetReasonForTest,
43
+ sendViaWebSocket,
40
44
  } from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
41
45
  import {
42
46
  _withCodexWsClientMetadata,
@@ -69,9 +73,198 @@ import { PATCH_TOOL_DEFS } from '../src/runtime/agent/orchestrator/tools/patch-t
69
73
  import { sendViaHttpSse } from '../src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs';
70
74
  import { buildRequestBody as buildOpenAIOAuthRequestBody } from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
71
75
  import { _convertMessagesToResponsesInputForTest } from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
76
+ import { OpenAIDirectProvider } from '../src/runtime/agent/orchestrator/providers/openai-ws.mjs';
72
77
 
73
78
  // --- Helpers ---------------------------------------------------------------
74
79
 
80
+ test('OpenAI API-key request keeps public defaults without network under every transport mode', async (t) => {
81
+ const priorTransport = process.env.MIXDOG_OAI_TRANSPORT;
82
+ t.after(() => {
83
+ if (priorTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
84
+ else process.env.MIXDOG_OAI_TRANSPORT = priorTransport;
85
+ });
86
+ const provider = new OpenAIDirectProvider({ apiKey: 'fixture-openai-key' });
87
+ for (const mode of ['auto', 'ws-full', 'ws-delta', 'http-sse']) {
88
+ process.env.MIXDOG_OAI_TRANSPORT = mode;
89
+ const calls = [];
90
+ let captured = null;
91
+ const result = await provider.send(
92
+ [{ role: 'user', content: 'fixture' }],
93
+ 'gpt-5.4',
94
+ [],
95
+ {
96
+ sessionId: `direct-request-defaults-${mode}`,
97
+ _fetchFn: async () => { throw new Error('global fetch seam must not run'); },
98
+ _sendViaWebSocketFn: async (request) => {
99
+ calls.push('ws');
100
+ captured = request;
101
+ return { content: 'ws-ok', toolCalls: [] };
102
+ },
103
+ _sendViaHttpSseFn: async (request) => {
104
+ calls.push('http');
105
+ captured = request;
106
+ return { content: 'http-ok', toolCalls: [] };
107
+ },
108
+ },
109
+ );
110
+ assert.deepEqual(calls, mode === 'http-sse' ? ['http'] : ['ws'], mode);
111
+ assert.equal(result.content, mode === 'http-sse' ? 'http-ok' : 'ws-ok');
112
+ assert.equal(captured.auth.type, 'openai-direct');
113
+ assert.equal(captured.auth.apiKey, 'fixture-openai-key');
114
+ if (mode !== 'http-sse') assert.equal(captured.traceProvider, 'openai-direct');
115
+ assert.equal(captured.body.store, true);
116
+ assert.equal(captured.body.prompt_cache_retention, '24h');
117
+ assert.equal(captured.body.stream, true);
118
+ }
119
+ });
120
+
121
+ function directHandshakeError(status) {
122
+ return Object.assign(new Error(`handshake ${status}`), { httpStatus: status });
123
+ }
124
+
125
+ function directWsEntry() {
126
+ return { socket: { close() {} }, ephemeral: true };
127
+ }
128
+
129
+ test('OpenAI API-key unsupported handshake 403/404/429 falls back once without nested WS retries', async (t) => {
130
+ const priorTransport = process.env.MIXDOG_OAI_TRANSPORT;
131
+ process.env.MIXDOG_OAI_TRANSPORT = 'auto';
132
+ t.after(() => {
133
+ if (priorTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
134
+ else process.env.MIXDOG_OAI_TRANSPORT = priorTransport;
135
+ });
136
+ for (const status of [403, 404, 429]) {
137
+ const provider = new OpenAIDirectProvider({ apiKey: 'fixture-openai-key' });
138
+ let acquires = 0;
139
+ let httpCalls = 0;
140
+ const result = await provider.send([], 'gpt-5.4', [], {
141
+ _fetchFn: async () => { throw new Error('global fetch seam must not run'); },
142
+ _webSocketTestSeams: {
143
+ // Neither null nor an inverted caller policy may override the
144
+ // direct provider's mandatory handshake policy.
145
+ handshakeErrorPolicy: status === 403
146
+ ? null
147
+ : () => ({ retry: true, httpFallback: false }),
148
+ _acquireWithRetryFn: async () => {
149
+ acquires += 1;
150
+ throw directHandshakeError(status);
151
+ },
152
+ _sleepFn: async () => {},
153
+ _sendSpanTraceFn: () => {},
154
+ _agentTraceFn: () => {},
155
+ },
156
+ _sendViaHttpSseFn: async () => {
157
+ httpCalls += 1;
158
+ return { content: `http-${status}`, toolCalls: [] };
159
+ },
160
+ });
161
+ assert.equal(result.content, `http-${status}`);
162
+ assert.equal(acquires, 1, `handshake ${status} must not retry WS`);
163
+ assert.equal(httpCalls, 1, `handshake ${status} gets one HTTP fallback`);
164
+ }
165
+ });
166
+
167
+ test('OpenAI API-key every application 4xx and exposed output never replay or fall back', async (t) => {
168
+ const priorTransport = process.env.MIXDOG_OAI_TRANSPORT;
169
+ process.env.MIXDOG_OAI_TRANSPORT = 'auto';
170
+ t.after(() => {
171
+ if (priorTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
172
+ else process.env.MIXDOG_OAI_TRANSPORT = priorTransport;
173
+ });
174
+ const cases = [
175
+ ...[400, 401, 402, 403, 404, 409, 418, 422, 429, 451, 499]
176
+ .map((status) => ({ status, exposed: null })),
177
+ { status: 500, exposed: 'text' },
178
+ { status: 500, exposed: 'tool' },
179
+ ];
180
+ for (const { status, exposed } of cases) {
181
+ const provider = new OpenAIDirectProvider({ apiKey: 'fixture-openai-key' });
182
+ let acquires = 0;
183
+ let streams = 0;
184
+ let httpCalls = 0;
185
+ let reloads = 0;
186
+ let visibleTextDeltas = 0;
187
+ let dispatchedToolCalls = 0;
188
+ provider.reloadApiKey = () => {
189
+ reloads += 1;
190
+ return 'replacement-key';
191
+ };
192
+ await assert.rejects(provider.send([], 'gpt-5.4', [], {
193
+ onTextDelta: () => { visibleTextDeltas += 1; },
194
+ onToolCall: () => { dispatchedToolCalls += 1; },
195
+ _fetchFn: async () => { throw new Error('global fetch seam must not run'); },
196
+ _webSocketTestSeams: {
197
+ _acquireWithRetryFn: async () => {
198
+ acquires += 1;
199
+ return { entry: directWsEntry(), reused: false };
200
+ },
201
+ _sendFrameFn: async () => {},
202
+ _streamFn: async ({ state, onTextDelta, onToolCall }) => {
203
+ streams += 1;
204
+ if (exposed === 'text') {
205
+ onTextDelta?.('visible-once');
206
+ state.emittedText = true;
207
+ }
208
+ if (exposed === 'tool') {
209
+ onToolCall?.({ id: 'tool-once', name: 'read', arguments: {} });
210
+ state.emittedToolCall = true;
211
+ }
212
+ throw Object.assign(new Error(`application ${status} ${exposed || ''}`), { httpStatus: status });
213
+ },
214
+ _sleepFn: async () => {},
215
+ _sendSpanTraceFn: () => {},
216
+ _agentTraceFn: () => {},
217
+ },
218
+ _sendViaHttpSseFn: async () => {
219
+ httpCalls += 1;
220
+ throw new Error('application/visible output must not reach HTTP');
221
+ },
222
+ }), new RegExp(`application ${status}`));
223
+ assert.equal(acquires, 1);
224
+ assert.equal(streams, 1);
225
+ assert.equal(httpCalls, 0);
226
+ assert.equal(reloads, 0);
227
+ assert.equal(visibleTextDeltas, exposed === 'text' ? 1 : 0);
228
+ assert.equal(dispatchedToolCalls, exposed === 'tool' ? 1 : 0);
229
+ }
230
+ });
231
+
232
+ test('Anthropic API-key gates deferred beta to requests that carry deferred tools', async () => {
233
+ const provider = Object.create((await import('../src/runtime/agent/orchestrator/providers/anthropic.mjs')).AnthropicProvider.prototype);
234
+ provider.name = 'anthropic';
235
+ provider.config = {};
236
+ provider.fastModeBetaHeaderLatched = false;
237
+ const calls = [];
238
+ provider.client = { messages: { create(params, options) {
239
+ calls.push({ params, options });
240
+ return { asResponse: async () => ({
241
+ ...anthropicSseResponse([
242
+ { type: 'message_start', message: { model: params.model, usage: { input_tokens: 1 } } },
243
+ { type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
244
+ { type: 'message_stop' },
245
+ ]),
246
+ ok: true,
247
+ status: 200,
248
+ headers: new Map(),
249
+ }) };
250
+ } } };
251
+
252
+ await provider._doSend([{ role: 'user', content: 'plain' }], 'claude-sonnet-4-6', [], {});
253
+ assert.doesNotMatch(calls[0].options.headers['anthropic-beta'], /advanced-tool-use/);
254
+
255
+ const active = [{ name: 'load_tool', description: 'load', inputSchema: { type: 'object', properties: {} } }];
256
+ const deferred = { name: 'mcp__demo__ping', description: 'ping', inputSchema: { type: 'object', properties: {} } };
257
+ await provider._doSend([{ role: 'user', content: 'load' }], 'claude-sonnet-4-6', active, {
258
+ session: {
259
+ deferredNativeTools: true,
260
+ deferredDiscoveredTools: [deferred.name],
261
+ deferredToolCatalog: [...active, deferred],
262
+ },
263
+ });
264
+ assert.match(calls[1].options.headers['anthropic-beta'], /advanced-tool-use/);
265
+ assert.equal(calls[1].params.tools.find((tool) => tool.name === deferred.name)?.defer_loading, true);
266
+ });
267
+
75
268
  test('native deferred history normalizes per provider without leaking OpenAI variants to xAI', () => {
76
269
  const nativePayload = {
77
270
  provider: 'openai-oauth',
@@ -1045,6 +1238,43 @@ test('anthropic API-key and OAuth lower plain signed thinkingBlocks before text
1045
1238
  }
1046
1239
  });
1047
1240
 
1241
+ test('anthropic API-key and OAuth lower interrupted tool results with is_error', () => {
1242
+ const history = [
1243
+ { role: 'user', content: 'run a tool' },
1244
+ {
1245
+ role: 'assistant',
1246
+ content: '',
1247
+ toolCalls: [{ id: 'toolu_interrupted_1', name: 'read', arguments: { path: 'x' } }],
1248
+ },
1249
+ {
1250
+ role: 'tool',
1251
+ content: 'Interrupted by user',
1252
+ toolCallId: 'toolu_interrupted_1',
1253
+ toolKind: 'error',
1254
+ },
1255
+ { role: 'user', content: '[Request interrupted by user]' },
1256
+ ];
1257
+ const apiKeyMessages = _toAnthropicMessagesForTest(history);
1258
+ const oauthMessages = _buildRequestBodyForCacheSmoke(
1259
+ history,
1260
+ 'claude-sonnet-4-6',
1261
+ [],
1262
+ {},
1263
+ ).messages;
1264
+
1265
+ for (const lowered of [apiKeyMessages, oauthMessages]) {
1266
+ const toolResult = lowered
1267
+ .flatMap((message) => Array.isArray(message.content) ? message.content : [])
1268
+ .find((block) => (
1269
+ block?.type === 'tool_result'
1270
+ && block.tool_use_id === 'toolu_interrupted_1'
1271
+ ));
1272
+ assert.ok(toolResult);
1273
+ assert.equal(toolResult.is_error, true);
1274
+ assert.equal(JSON.stringify(lowered).includes('[Request interrupted by user]'), true);
1275
+ }
1276
+ });
1277
+
1048
1278
  test('anthropic effort: legacy claude-3-7-sonnet gets NO adaptive thinking / effort beta', () => {
1049
1279
  const model = 'claude-3-7-sonnet-20250219';
1050
1280
  assert.equal(modelSupportsEffort(model), false);
@@ -1786,13 +2016,23 @@ test('openai oauth ws delta: ws-delta mode uses previous_response_id without tur
1786
2016
  entry: {
1787
2017
  ...entry,
1788
2018
  lastRequestInput: [body.input[0]],
1789
- lastResponseItems: [{ type: 'function_call', call_id: 'call_1', name: 'tool', arguments: '{}' }],
2019
+ lastResponseItems: [{
2020
+ type: 'function_call',
2021
+ call_id: 'call_1',
2022
+ name: 'tool',
2023
+ arguments: '{"api_key":"provider-function-secret","nested":{"z":2,"a":1}}',
2024
+ }],
1790
2025
  },
1791
2026
  body: {
1792
2027
  ...body,
1793
2028
  input: [
1794
2029
  body.input[0],
1795
- { type: 'function_call', call_id: 'call_other', name: 'tool', arguments: '{}' },
2030
+ {
2031
+ type: 'function_call',
2032
+ call_id: 'call_other',
2033
+ name: 'tool',
2034
+ arguments: '{"nested":{"a":1,"z":2},"api_key":"replayed-function-secret"}',
2035
+ },
1796
2036
  body.input[1],
1797
2037
  ],
1798
2038
  },
@@ -1800,6 +2040,193 @@ test('openai oauth ws delta: ws-delta mode uses previous_response_id without tur
1800
2040
  assert.equal(responseMismatch.mode, 'full');
1801
2041
  assert.equal(responseMismatch.reason, 'response_output_mismatch:function_call');
1802
2042
  assert.equal(responseMismatch.frame.previous_response_id, undefined);
2043
+ const mismatch = responseMismatch.responseOutputMismatch;
2044
+ assert.deepEqual({
2045
+ expectedType: mismatch.response_output_mismatch_expected_type,
2046
+ actualType: mismatch.response_output_mismatch_actual_type,
2047
+ expectedCount: mismatch.response_output_mismatch_expected_response_item_count,
2048
+ actualCount: mismatch.response_output_mismatch_actual_replayed_input_item_count,
2049
+ }, {
2050
+ expectedType: 'function_call',
2051
+ actualType: 'function_call',
2052
+ expectedCount: 1,
2053
+ actualCount: 2,
2054
+ });
2055
+ assert.match(mismatch.response_output_mismatch_expected_hash, /^[a-f0-9]{64}$/);
2056
+ assert.match(mismatch.response_output_mismatch_actual_hash, /^[a-f0-9]{64}$/);
2057
+ const traceSafe = JSON.stringify(mismatch);
2058
+ assert.equal(traceSafe.includes('call_other'), false);
2059
+ assert.equal(traceSafe.includes('provider-function-secret'), false);
2060
+ assert.equal(traceSafe.includes('replayed-function-secret'), false);
2061
+ } finally {
2062
+ if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
2063
+ else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
2064
+ }
2065
+ });
2066
+
2067
+ test('openai oauth ws delta: message mismatch diagnostics hash normalized content without tracing it', () => {
2068
+ const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
2069
+ try {
2070
+ process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
2071
+ const prior = { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'prior' }] };
2072
+ const expected = { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'provider secret' }] };
2073
+ const actual = { type: 'message', role: 'assistant', content: [{ type: 'input_text', text: 'replayed secret' }] };
2074
+ const delta = _computeDelta({
2075
+ entry: {
2076
+ lastRequestSansInput: '{"model":"gpt-5.5"}',
2077
+ lastResponseId: 'resp_prev',
2078
+ lastRequestInput: [prior],
2079
+ lastResponseItems: [expected],
2080
+ },
2081
+ body: { model: 'gpt-5.5', input: [prior, actual] },
2082
+ });
2083
+ const mismatch = delta.responseOutputMismatch;
2084
+ assert.equal(delta.reason, 'response_output_mismatch:message');
2085
+ assert.equal(mismatch.response_output_mismatch_expected_type, 'message');
2086
+ assert.equal(mismatch.response_output_mismatch_actual_type, 'message');
2087
+ assert.notEqual(mismatch.response_output_mismatch_expected_hash, mismatch.response_output_mismatch_actual_hash);
2088
+ const traceSafe = JSON.stringify(mismatch);
2089
+ assert.equal(traceSafe.includes('provider secret'), false);
2090
+ assert.equal(traceSafe.includes('replayed secret'), false);
2091
+ } finally {
2092
+ if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
2093
+ else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
2094
+ }
2095
+ });
2096
+
2097
+ test('openai oauth ws delta: diagnostic hashes normalize equivalent message and function-call forms', () => {
2098
+ const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
2099
+ try {
2100
+ process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
2101
+ const prior = { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'prior' }] };
2102
+ const mismatch = (expected, actual) => _computeDelta({
2103
+ entry: {
2104
+ lastRequestSansInput: '{"model":"gpt-5.5"}',
2105
+ lastResponseId: 'resp_prev',
2106
+ lastRequestInput: [prior],
2107
+ lastResponseItems: [expected],
2108
+ },
2109
+ body: { model: 'gpt-5.5', input: [prior, actual] },
2110
+ }).responseOutputMismatch;
2111
+ const message = { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'same sensitive message' }] };
2112
+ const messageExpected = mismatch(message, {
2113
+ type: 'message',
2114
+ role: 'assistant',
2115
+ content: [{ type: 'output_text', text: 'different message' }],
2116
+ });
2117
+ const messageActual = mismatch({
2118
+ type: 'message',
2119
+ role: 'assistant',
2120
+ content: [{ type: 'output_text', text: 'different message' }],
2121
+ }, {
2122
+ type: 'message',
2123
+ role: 'assistant',
2124
+ content: [{ type: 'input_text', text: 'same sensitive message' }],
2125
+ });
2126
+ assert.equal(
2127
+ messageExpected.response_output_mismatch_expected_hash,
2128
+ messageActual.response_output_mismatch_actual_hash,
2129
+ );
2130
+
2131
+ const functionExpected = (argumentsValue) => mismatch({
2132
+ type: 'function_call',
2133
+ call_id: 'call_same',
2134
+ name: 'tool',
2135
+ arguments: argumentsValue,
2136
+ }, {
2137
+ type: 'function_call',
2138
+ call_id: 'call_different',
2139
+ name: 'tool',
2140
+ arguments: '{"ignored":"replayed-function-secret"}',
2141
+ });
2142
+ assert.equal(
2143
+ functionExpected('{"api_key":"function-secret","nested":{"z":2,"a":1}}').response_output_mismatch_expected_hash,
2144
+ functionExpected('{"nested":{"a":1,"z":2},"api_key":"function-secret"}').response_output_mismatch_expected_hash,
2145
+ );
2146
+ } finally {
2147
+ if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
2148
+ else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
2149
+ }
2150
+ });
2151
+
2152
+ test('openai oauth ws delta: mismatch diagnostics reach transport and cache_break without sensitive values', async () => {
2153
+ const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
2154
+ try {
2155
+ process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
2156
+ const rows = [];
2157
+ const prior = { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'prior' }] };
2158
+ const cases = [{
2159
+ expected: {
2160
+ type: 'function_call',
2161
+ call_id: 'call_expected',
2162
+ name: 'tool',
2163
+ arguments: '{"api_key":"provider-function-secret","nested":{"z":2,"a":1}}',
2164
+ },
2165
+ actual: {
2166
+ type: 'function_call',
2167
+ call_id: 'call_actual',
2168
+ name: 'tool',
2169
+ arguments: '{"nested":{"a":1,"z":2},"api_key":"replayed-function-secret"}',
2170
+ },
2171
+ }, {
2172
+ expected: {
2173
+ type: 'custom_tool_call',
2174
+ call_id: 'custom_expected',
2175
+ name: 'apply_patch',
2176
+ input: 'provider-custom-input-secret',
2177
+ },
2178
+ actual: {
2179
+ type: 'custom_tool_call',
2180
+ call_id: 'custom_actual',
2181
+ name: 'apply_patch',
2182
+ input: 'replayed-custom-input-secret',
2183
+ },
2184
+ }];
2185
+ for (const { expected, actual } of cases) {
2186
+ const body = { model: 'gpt-5.5', input: [prior, actual] };
2187
+ const entry = {
2188
+ socket: { close() {} },
2189
+ lastRequestSansInput: _stableStringify(_sansInput(body)),
2190
+ lastResponseId: 'resp_prev',
2191
+ lastRequestInput: [prior],
2192
+ lastResponseItems: [expected],
2193
+ };
2194
+ const expectedDiagnostics = _computeDelta({ entry, body }).responseOutputMismatch;
2195
+ await sendViaWebSocket({
2196
+ auth: { type: 'xai', access_token: 'test-token' },
2197
+ body,
2198
+ poolKey: `mismatch-trace-${expected.type}`,
2199
+ cacheKey: 'mismatch-trace-test',
2200
+ iteration: 1,
2201
+ useModel: 'gpt-5.5',
2202
+ traceProvider: 'xai',
2203
+ _acquireWithRetryFn: async () => ({ entry, reused: false }),
2204
+ _sendFrameFn: async () => {},
2205
+ _streamFn: async () => ({
2206
+ content: 'ok',
2207
+ model: 'gpt-5.5',
2208
+ toolCalls: [],
2209
+ usage: {},
2210
+ responseId: 'resp_next',
2211
+ responseItems: [],
2212
+ closeSocket: true,
2213
+ }),
2214
+ _agentTraceFn: (row) => rows.push(row),
2215
+ });
2216
+ const emitted = rows.filter((row) => row.payload?.response_output_mismatch_expected_type === expected.type);
2217
+ assert.deepEqual(emitted.map((row) => row.kind).sort(), ['cache_break', 'transport']);
2218
+ for (const row of emitted) assert.deepEqual(
2219
+ Object.fromEntries(Object.keys(expectedDiagnostics).map((key) => [key, row.payload[key]])),
2220
+ expectedDiagnostics,
2221
+ );
2222
+ }
2223
+ const traceText = JSON.stringify(rows);
2224
+ for (const secret of [
2225
+ 'provider-function-secret',
2226
+ 'replayed-function-secret',
2227
+ 'provider-custom-input-secret',
2228
+ 'replayed-custom-input-secret',
2229
+ ]) assert.equal(traceText.includes(secret), false);
1803
2230
  } finally {
1804
2231
  if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
1805
2232
  else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
@@ -2165,13 +2592,100 @@ test('responses transport policy: xai ws-full → WS, http-sse → HTTP', () =>
2165
2592
  assert.equal(http.allowHttpFallback, false);
2166
2593
  });
2167
2594
 
2168
- test('openai-compat/xai: provider-local HTTP pin beats global OAI ws-delta', async () => {
2595
+ test('openai-compat/xai: HTTP pin uses only the injected preconnect seam', async () => {
2169
2596
  const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
2597
+ const prevFetch = globalThis.fetch;
2598
+ let outboundFetchAttempts = 0;
2599
+ let injectedPreconnectCalls = 0;
2170
2600
  try {
2171
2601
  process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
2602
+ globalThis.fetch = async () => {
2603
+ outboundFetchAttempts += 1;
2604
+ throw new Error('provider transport test attempted outbound fetch');
2605
+ };
2606
+
2607
+ // Resolve the actual imported binding and inspect _doSend before
2608
+ // invoking any outbound-capable provider path. AST traversal naturally
2609
+ // excludes comments/strings and catches direct, aliased, parenthesized,
2610
+ // optional, or assigned references.
2611
+ const compatSource = readFileSync(
2612
+ new URL('../src/runtime/agent/orchestrator/providers/openai-compat.mjs', import.meta.url),
2613
+ 'utf8',
2614
+ );
2615
+ const compatAst = parse(compatSource, {
2616
+ ecmaVersion: 'latest',
2617
+ sourceType: 'module',
2618
+ locations: true,
2619
+ ranges: true,
2620
+ });
2621
+ const preconnectImport = compatAst.body
2622
+ .filter(node => node.type === 'ImportDeclaration')
2623
+ .flatMap(node => node.specifiers)
2624
+ .find(specifier =>
2625
+ specifier.type === 'ImportSpecifier'
2626
+ && specifier.imported.name === 'preconnect',
2627
+ );
2628
+ assert.ok(preconnectImport, 'shared preconnect import binding must be resolvable');
2629
+ const scopeManager = analyze(compatAst, { ecmaVersion: 2022, sourceType: 'module' });
2630
+ const moduleScope = scopeManager.scopes.find(scope => scope.type === 'module');
2631
+ const preconnectBinding = moduleScope?.set.get(preconnectImport.local.name);
2632
+ assert.ok(preconnectBinding, 'shared preconnect source binding must be resolvable');
2633
+
2634
+ const providerClass = compatAst.body
2635
+ .map(node => node.type === 'ExportNamedDeclaration' ? node.declaration : node)
2636
+ .find(node =>
2637
+ node?.type === 'ClassDeclaration'
2638
+ && node.id?.name === 'OpenAICompatProvider',
2639
+ );
2640
+ assert.ok(providerClass, 'OpenAICompatProvider class must be resolvable');
2641
+ const doSendMethod = providerClass.body.body.find(node =>
2642
+ node.type === 'MethodDefinition'
2643
+ && node.key?.type === 'Identifier'
2644
+ && node.key.name === '_doSend',
2645
+ );
2646
+ assert.ok(doSendMethod, 'OpenAICompatProvider._doSend must be resolvable');
2647
+
2648
+ let callsInjectedInstanceMember = false;
2649
+ const visit = node => {
2650
+ if (!node || typeof node !== 'object') return;
2651
+ if (
2652
+ node.type === 'CallExpression'
2653
+ && node.callee?.type === 'MemberExpression'
2654
+ && node.callee.object?.type === 'ThisExpression'
2655
+ && node.callee.computed === false
2656
+ && node.callee.property?.name === '_preconnectFn'
2657
+ ) {
2658
+ callsInjectedInstanceMember = true;
2659
+ }
2660
+ for (const [key, value] of Object.entries(node)) {
2661
+ if (key === 'start' || key === 'end') continue;
2662
+ if (Array.isArray(value)) value.forEach(visit);
2663
+ else if (value && typeof value === 'object') visit(value);
2664
+ }
2665
+ };
2666
+ visit(doSendMethod.value.body);
2667
+ const importedBindingReferences = preconnectBinding.references.filter(reference =>
2668
+ reference.identifier.start >= doSendMethod.value.body.start
2669
+ && reference.identifier.end <= doSendMethod.value.body.end,
2670
+ );
2671
+ assert.equal(
2672
+ importedBindingReferences.length,
2673
+ 0,
2674
+ '_doSend must not reference the shared preconnect import binding',
2675
+ );
2676
+ assert.equal(
2677
+ callsInjectedInstanceMember,
2678
+ true,
2679
+ '_doSend must call the instance preconnect seam',
2680
+ );
2681
+
2172
2682
  const provider = new OpenAICompatProvider('xai', {
2173
2683
  apiKey: 'xai-test',
2174
2684
  responsesTransport: 'http',
2685
+ preconnect: true,
2686
+ preconnectFn: () => {
2687
+ injectedPreconnectCalls += 1;
2688
+ },
2175
2689
  });
2176
2690
  let httpCalled = false;
2177
2691
  provider._doSendXaiResponses = async () => {
@@ -2184,7 +2698,10 @@ test('openai-compat/xai: provider-local HTTP pin beats global OAI ws-delta', asy
2184
2698
  const result = await provider._doSend([{ role: 'user', content: 'hi' }], 'grok-build', [], {});
2185
2699
  assert.equal(result.content, 'ok');
2186
2700
  assert.equal(httpCalled, true);
2701
+ assert.equal(injectedPreconnectCalls, 1, 'only the injected preconnect seam must run');
2702
+ assert.equal(outboundFetchAttempts, 0, 'stubbed provider test must remain network hermetic');
2187
2703
  } finally {
2704
+ globalThis.fetch = prevFetch;
2188
2705
  if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
2189
2706
  else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
2190
2707
  }
@@ -141,4 +141,63 @@ assert(reloaded.compaction?.lastStage === 'overflow_failed', `compaction lastSta
141
141
  assert(reloaded.providerState?.xaiResponses?.previousResponseId === 'stale-after-compact',
142
142
  'providerState should remain when a failed reactive compact leaves the transcript unchanged');
143
143
 
144
+ // Successful pre-send compaction must persist an explicit providerState clear.
145
+ // The following ask then proves the stale continuation is absent from the
146
+ // provider options, not merely absent from the first loop's local variable.
147
+ let successfulCompactSends = 0;
148
+ const nextAskStates = [];
149
+ provider.send = async (_sentMessages, _model, _tools, opts = {}) => {
150
+ if (String(opts?.sessionId || '').endsWith(':compact')) {
151
+ successfulCompactSends += 1;
152
+ return {
153
+ content: [
154
+ '## Goal',
155
+ '- verify provider state clear',
156
+ '',
157
+ '## Progress',
158
+ '### Done',
159
+ '- old history compacted',
160
+ '',
161
+ '## Next Steps',
162
+ '- continue',
163
+ ].join('\n'),
164
+ };
165
+ }
166
+ nextAskStates.push(opts.providerState);
167
+ return { content: 'successful answer', stopReason: 'end_turn' };
168
+ };
169
+ const clearSessionId = `sess_provider_state_clear_${process.pid}_${Date.now()}`;
170
+ const clearMessages = [{ role: 'system', content: 'rules' }];
171
+ for (let i = 0; i < 10; i += 1) {
172
+ clearMessages.push({ role: 'user', content: `old ${i} ${'x '.repeat(500)}` });
173
+ clearMessages.push({ role: 'assistant', content: `answer ${i} ${'y '.repeat(500)}` });
174
+ }
175
+ await saveSessionAsync({
176
+ id: clearSessionId,
177
+ provider: 'openai-oauth',
178
+ model: 'provider-state-clear-smoke',
179
+ owner: 'agent',
180
+ agent: 'heavy-worker',
181
+ messages: clearMessages,
182
+ tools: [],
183
+ generation: 0,
184
+ closed: false,
185
+ contextWindow: 12_000,
186
+ rawContextWindow: 12_000,
187
+ compactBoundaryTokens: 12_000,
188
+ autoCompactTokenLimit: 4_000,
189
+ compaction: { auto: true, semantic: true, recallFastTrack: false, type: 1, compactType: 1 },
190
+ cwd: process.cwd(),
191
+ sessionStartMetaInjected: true,
192
+ providerState: { xaiResponses: { previousResponseId: 'must-clear' } },
193
+ }, { expectedGeneration: 0 });
194
+ await askSession(clearSessionId, 'first ask', null, null, process.cwd());
195
+ const afterClear = loadSession(clearSessionId);
196
+ assert(!Object.hasOwn(afterClear, 'providerState'), 'successful compact must remove providerState from persisted session');
197
+ await askSession(clearSessionId, 'second ask', null, null, process.cwd());
198
+ assert(successfulCompactSends >= 1, 'fixture must perform successful semantic compaction');
199
+ assert(nextAskStates.length >= 2, 'both asks must reach provider');
200
+ assert(nextAskStates[0] === undefined, 'post-compact send must receive cleared providerState');
201
+ assert(nextAskStates[1] === undefined, 'next ask must not resurrect stale providerState');
202
+
144
203
  process.stdout.write('reactive-compact-persist smoke passed ✓\n');