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.
- package/package.json +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +55 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/hook-bus-test.mjs +23 -0
- package/scripts/internal-tools-normalization-test.mjs +10 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +76 -0
- package/scripts/max-output-recovery-test.mjs +55 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +391 -4
- package/scripts/process-lifecycle-test.mjs +389 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +268 -0
- package/scripts/provider-toolcall-test.mjs +520 -3
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-bench-cache-break-test.mjs +102 -0
- package/scripts/session-bench.mjs +101 -42
- package/scripts/shell-failure-diagnostics-test.mjs +73 -4
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/smoke-loop-failure-summary-test.mjs +38 -0
- package/scripts/smoke-loop-failure-summary.mjs +16 -0
- package/scripts/smoke-loop.mjs +2 -7
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/tool-failures.mjs +15 -1
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +43 -7
- package/scripts/web-fetch-routing-test.mjs +158 -0
- package/src/cli.mjs +15 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +26 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -0
- package/src/runtime/agent/orchestrator/internal-tools.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +118 -26
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +29 -17
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -38
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +24 -4
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +95 -46
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +89 -17
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +93 -26
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +144 -51
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +74 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +111 -6
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +13 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +224 -104
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +127 -55
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +17 -1
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +69 -37
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +6 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +24 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +89 -22
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +380 -37
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -14
- package/src/runtime/memory/index.mjs +22 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -13
- package/src/runtime/search/index.mjs +41 -0
- package/src/runtime/search/lib/http-fetch.mjs +154 -0
- package/src/runtime/search/tool-defs.mjs +23 -0
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/process-lifecycle.mjs +363 -0
- package/src/runtime/shared/process-shutdown.mjs +27 -4
- package/src/runtime/shared/resource-admission.mjs +359 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/lifecycle-api.mjs +26 -6
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +43 -18
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog.mjs +375 -15
- package/src/standalone/agent-tool.mjs +17 -38
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/standalone/hook-bus/handlers.mjs +4 -0
- package/src/standalone/hook-bus/rules.mjs +7 -1
- package/src/standalone/hook-bus.mjs +10 -2
- package/src/tui/App.jsx +17 -11
- package/src/tui/app/live-spinner-visibility.mjs +20 -0
- package/src/tui/dist/index.mjs +101 -281
- package/src/tui/engine/session-api-ext.mjs +13 -2
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +4 -1
- 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: [{
|
|
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
|
-
{
|
|
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:
|
|
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');
|