mixdog 0.9.6 → 0.9.8
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/README.md +4 -4
- package/package.json +1 -1
- package/scripts/provider-toolcall-test.mjs +88 -0
- package/src/rules/agent/30-explorer.md +20 -14
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +30 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +6 -1
- package/src/standalone/explore-tool.mjs +5 -6
package/README.md
CHANGED
|
@@ -32,14 +32,14 @@ repo-focused tools for reading, editing, testing, and reviewing code.
|
|
|
32
32
|
## Install
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
npm install
|
|
35
|
+
npm install -g mixdog
|
|
36
|
+
mixdog --help
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
For local development from this checkout:
|
|
39
40
|
|
|
40
41
|
```bash
|
|
41
|
-
npm install
|
|
42
|
-
mixdog --help
|
|
42
|
+
npm install
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
## Run
|
package/package.json
CHANGED
|
@@ -24,6 +24,12 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
consumeCompatChatCompletionStream,
|
|
26
26
|
} from '../src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs';
|
|
27
|
+
import {
|
|
28
|
+
_computeDelta,
|
|
29
|
+
} from '../src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs';
|
|
30
|
+
import {
|
|
31
|
+
_cacheObservationForTest,
|
|
32
|
+
} from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
|
|
27
33
|
import {
|
|
28
34
|
createGeminiTextLeakGuard,
|
|
29
35
|
parseToolCalls as geminiParseToolCalls,
|
|
@@ -976,3 +982,85 @@ test('anthropic effort: explicit thinkingBudgetTokens wins over effort', () => {
|
|
|
976
982
|
assert.equal(body.output_config, undefined);
|
|
977
983
|
assert.equal(shouldIncludeEffortBeta(model, { effort: 'low', thinkingBudgetTokens: 2048 }), false);
|
|
978
984
|
});
|
|
985
|
+
// === 9. OpenAI OAuth WS cache tracing ======================================
|
|
986
|
+
|
|
987
|
+
test('openai oauth ws delta: default full frame is an expected mode, not a cache break', () => {
|
|
988
|
+
const previous = process.env.MIXDOG_OAI_WS_DELTA;
|
|
989
|
+
try {
|
|
990
|
+
delete process.env.MIXDOG_OAI_WS_DELTA;
|
|
991
|
+
const body = { model: 'gpt-5.5', input: [{ type: 'message', role: 'user', content: 'hi' }] };
|
|
992
|
+
const delta = _computeDelta({
|
|
993
|
+
entry: {
|
|
994
|
+
lastRequestSansInput: '{}',
|
|
995
|
+
lastResponseId: 'resp_prev',
|
|
996
|
+
lastRequestInput: body.input,
|
|
997
|
+
lastResponseItems: [],
|
|
998
|
+
},
|
|
999
|
+
body,
|
|
1000
|
+
});
|
|
1001
|
+
assert.equal(delta.mode, 'full');
|
|
1002
|
+
assert.equal(delta.reason, 'full_default');
|
|
1003
|
+
assert.equal(delta.frame.previous_response_id, undefined);
|
|
1004
|
+
} finally {
|
|
1005
|
+
if (previous == null) delete process.env.MIXDOG_OAI_WS_DELTA;
|
|
1006
|
+
else process.env.MIXDOG_OAI_WS_DELTA = previous;
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
test('openai oauth ws delta: opt-in still falls back without turn-state unless forced', () => {
|
|
1011
|
+
const previous = process.env.MIXDOG_OAI_WS_DELTA;
|
|
1012
|
+
try {
|
|
1013
|
+
const body = {
|
|
1014
|
+
model: 'gpt-5.5',
|
|
1015
|
+
input: [
|
|
1016
|
+
{ type: 'message', role: 'user', content: 'prev' },
|
|
1017
|
+
{ type: 'message', role: 'user', content: 'next' },
|
|
1018
|
+
],
|
|
1019
|
+
};
|
|
1020
|
+
const entry = {
|
|
1021
|
+
lastRequestSansInput: '{"model":"gpt-5.5"}',
|
|
1022
|
+
lastResponseId: 'resp_prev',
|
|
1023
|
+
lastRequestInput: [body.input[0]],
|
|
1024
|
+
lastResponseItems: [],
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
process.env.MIXDOG_OAI_WS_DELTA = '1';
|
|
1028
|
+
const safe = _computeDelta({ entry, body });
|
|
1029
|
+
assert.equal(safe.mode, 'full');
|
|
1030
|
+
assert.equal(safe.reason, 'delta_missing_turn_state');
|
|
1031
|
+
assert.equal(safe.frame.previous_response_id, undefined);
|
|
1032
|
+
|
|
1033
|
+
process.env.MIXDOG_OAI_WS_DELTA = 'force';
|
|
1034
|
+
const forced = _computeDelta({ entry, body });
|
|
1035
|
+
assert.equal(forced.mode, 'delta');
|
|
1036
|
+
assert.equal(forced.frame.previous_response_id, 'resp_prev');
|
|
1037
|
+
assert.deepEqual(forced.frame.input, [body.input[1]]);
|
|
1038
|
+
} finally {
|
|
1039
|
+
if (previous == null) delete process.env.MIXDOG_OAI_WS_DELTA;
|
|
1040
|
+
else process.env.MIXDOG_OAI_WS_DELTA = previous;
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
test('openai oauth ws cache observation detects warm zero and partial retreats', () => {
|
|
1044
|
+
const zero = _cacheObservationForTest({
|
|
1045
|
+
entry: { promptCacheMaxCachedTokens: 47_616 },
|
|
1046
|
+
result: { usage: { inputTokens: 59_000, promptTokens: 59_000, cachedTokens: 0 } },
|
|
1047
|
+
});
|
|
1048
|
+
assert.equal(zero.actualMiss, true);
|
|
1049
|
+
assert.equal(zero.missReason, 'warm_session_zero_cached_tokens');
|
|
1050
|
+
assert.equal(zero.uncachedTokens, 59_000);
|
|
1051
|
+
|
|
1052
|
+
const partial = _cacheObservationForTest({
|
|
1053
|
+
entry: { promptCacheMaxCachedTokens: 101_888 },
|
|
1054
|
+
result: { usage: { inputTokens: 102_456, promptTokens: 102_456, cachedTokens: 60_928 } },
|
|
1055
|
+
});
|
|
1056
|
+
assert.equal(partial.actualMiss, true);
|
|
1057
|
+
assert.equal(partial.missReason, 'warm_session_cached_tokens_dropped');
|
|
1058
|
+
assert.equal(partial.cachedTokens % 512, 0);
|
|
1059
|
+
|
|
1060
|
+
const healthy = _cacheObservationForTest({
|
|
1061
|
+
entry: { promptCacheMaxCachedTokens: 101_888 },
|
|
1062
|
+
result: { usage: { inputTokens: 106_468, promptTokens: 106_468, cachedTokens: 103_424 } },
|
|
1063
|
+
});
|
|
1064
|
+
assert.equal(healthy.actualMiss, false);
|
|
1065
|
+
assert.equal(healthy.missReason, null);
|
|
1066
|
+
});
|
|
@@ -6,23 +6,29 @@ kind: retrieval
|
|
|
6
6
|
|
|
7
7
|
# Role: explorer
|
|
8
8
|
|
|
9
|
-
You are a one-shot locator, not a researcher.
|
|
9
|
+
You are a one-shot locator, not a researcher. You find WHERE, never WHY or
|
|
10
|
+
HOW: no reading code to understand it, no explaining behaviour, no tracing
|
|
11
|
+
logic across files. Coordinates are the entire deliverable.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
Default shape: ONE tool turn, then the answer. Turn 1 is ONE maximal batch:
|
|
14
|
+
a single `grep` carrying ALL literal strings/errors/identifier guesses
|
|
15
|
+
(content mode, small `-C`), plus `code_graph`/`find`/`glob` in the same
|
|
16
|
+
turn. Fire everything at once; never probe one tool at a time. Any second
|
|
17
|
+
turn means turn 1 under-batched.
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
candidate lookups in one shot.
|
|
19
|
+
Answer straight from search output: grep/code_graph hits already carry
|
|
20
|
+
`path:line` — NEVER spend a turn on `read` just to confirm or polish an
|
|
21
|
+
anchor you already have. Prefer user-visible strings and identifiers.
|
|
22
|
+
`dist`/generated hits are leads, not answers: trace them back to source.
|
|
20
23
|
|
|
21
|
-
Budget:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
Budget: 5 tool turns; coordinates only, zero analysis. The FIRST
|
|
25
|
+
credible `path:line` ends the search — answer in that same turn; do not
|
|
26
|
+
verify, refine, or collect extras. A plausible anchor now beats a perfect
|
|
27
|
+
anchor later: mark weak ones `?` instead of checking them. Turns 2-5 exist
|
|
28
|
+
ONLY for a zero-anchor turn 1: derive new tokens from returned paths/names
|
|
29
|
+
or widen scope (no repeated synonyms), batching ALL remaining candidates in
|
|
30
|
+
one shot. Past turn 5 you are wasting the caller's time: answer with
|
|
31
|
+
best-so-far anchors or `EXPLORATION_FAILED`.
|
|
26
32
|
|
|
27
33
|
Answer format, nothing else:
|
|
28
34
|
- up to 5 lines: `path:line — symbol/name — short reason` (append `?` if weak)
|
|
@@ -72,6 +72,7 @@ export {
|
|
|
72
72
|
_logicalResponseItemMatch,
|
|
73
73
|
parseToolSearchArgs,
|
|
74
74
|
_streamResponse,
|
|
75
|
+
_cacheObservation as _cacheObservationForTest,
|
|
75
76
|
};
|
|
76
77
|
|
|
77
78
|
globalThis.__mixdogOpenaiWsRuntimeLoaded = true;
|
|
@@ -293,6 +294,34 @@ function _codexWsCompatibilityHeaders(context = {}) {
|
|
|
293
294
|
// body client_metadata; the server may gate x-codex-turn-state issuance
|
|
294
295
|
// on handshake-level client identity, so mirror codex here.
|
|
295
296
|
if (metadata['x-codex-installation-id']) headers['x-codex-installation-id'] = metadata['x-codex-installation-id'];
|
|
297
|
+
// Turn-metadata handshake header. codex's compatibility_headers()
|
|
298
|
+
// (responses_metadata.rs:227-252) attaches x-codex-turn-metadata /
|
|
299
|
+
// x-codex-parent-thread-id / x-codex-window-id on the request. A/B
|
|
300
|
+
// (2026-07-04, tm0b vs tm1b, 10 parallel-5 sessions each, gpt-5.5):
|
|
301
|
+
// WITH the turn-metadata blob it2 cache misses dropped 4/10 -> 0/10 and
|
|
302
|
+
// full-prefix hits rose 2/10 -> 7/10; x-codex-turn-state was never issued
|
|
303
|
+
// in either arm, so the win comes from better cache routing on the
|
|
304
|
+
// handshake fingerprint, not sticky turn-state. Default ON.
|
|
305
|
+
// MIXDOG_OAI_TURN_METADATA overrides:
|
|
306
|
+
// unset / 1 / turn-metadata : window-id + turn-metadata + installation-id
|
|
307
|
+
// parent : + x-codex-parent-thread-id (= thread_id)
|
|
308
|
+
// window : window-id only (drop turn-metadata blob)
|
|
309
|
+
// 0 / off / false / no : strip the blob (pre-2026-07-04 baseline)
|
|
310
|
+
const probe = String(process.env.MIXDOG_OAI_TURN_METADATA || '').trim().toLowerCase();
|
|
311
|
+
if (probe === '0' || probe === 'off' || probe === 'false' || probe === 'no') {
|
|
312
|
+
// Not opted in: keep window-id + installation-id but drop the full
|
|
313
|
+
// turn-metadata blob so we match the historical (turn-state-never-issued)
|
|
314
|
+
// baseline exactly.
|
|
315
|
+
delete headers['x-codex-turn-metadata'];
|
|
316
|
+
} else if (probe === 'parent') {
|
|
317
|
+
const parentThreadId = _cleanMetaString(context?.sendOpts?.parentThreadId
|
|
318
|
+
|| context?.sendOpts?.codexParentThreadId
|
|
319
|
+
|| metadata.thread_id);
|
|
320
|
+
if (parentThreadId) headers['x-codex-parent-thread-id'] = parentThreadId;
|
|
321
|
+
} else if (probe === 'window') {
|
|
322
|
+
delete headers['x-codex-turn-metadata'];
|
|
323
|
+
}
|
|
324
|
+
// probe === '1' | 'true' | 'yes' | 'on' | 'turn-metadata' => full set as built above.
|
|
296
325
|
return headers;
|
|
297
326
|
}
|
|
298
327
|
|
|
@@ -1060,7 +1089,7 @@ export async function sendViaWebSocket({
|
|
|
1060
1089
|
});
|
|
1061
1090
|
const chainFallback = mode !== 'delta'
|
|
1062
1091
|
&& deltaReason
|
|
1063
|
-
&& !['no_anchor', 'full_forced'].includes(deltaReason);
|
|
1092
|
+
&& !['no_anchor', 'full_forced', 'full_default', 'delta_missing_turn_state'].includes(deltaReason);
|
|
1064
1093
|
if (chainFallback || (mode === 'delta' && deltaReason)) {
|
|
1065
1094
|
appendAgentTrace({
|
|
1066
1095
|
sessionId: poolKey,
|
|
@@ -179,13 +179,18 @@ export function _computeDelta({ entry, body }) {
|
|
|
179
179
|
// token, previous_response_id requests hop cache nodes and only the first
|
|
180
180
|
// prefix blocks hit. Re-enable delta explicitly via MIXDOG_OAI_WS_DELTA=1
|
|
181
181
|
// for future probes if the backend starts issuing turn-state.
|
|
182
|
-
const
|
|
182
|
+
const deltaMode = String(process.env.MIXDOG_OAI_WS_DELTA || '').trim().toLowerCase();
|
|
183
|
+
const deltaForce = ['force', 'unsafe', 'always'].includes(deltaMode);
|
|
184
|
+
const deltaOptIn = deltaForce || ['1', 'true', 'yes', 'on'].includes(deltaMode);
|
|
183
185
|
if (!deltaOptIn) {
|
|
184
186
|
return { mode: 'full', reason: 'full_default', frame: { type: 'response.create', ...body } };
|
|
185
187
|
}
|
|
186
188
|
if (!entry || !entry.lastRequestSansInput || !entry.lastResponseId) {
|
|
187
189
|
return { mode: 'full', reason: 'no_anchor', frame: { type: 'response.create', ...body } };
|
|
188
190
|
}
|
|
191
|
+
if (!deltaForce && !entry.turnState) {
|
|
192
|
+
return { mode: 'full', reason: 'delta_missing_turn_state', frame: { type: 'response.create', ...body } };
|
|
193
|
+
}
|
|
189
194
|
if (!Array.isArray(entry.lastRequestInput)) {
|
|
190
195
|
return { mode: 'full', reason: 'no_input_snapshot', frame: { type: 'response.create', ...body } };
|
|
191
196
|
}
|
|
@@ -70,7 +70,7 @@ function escapeXml(str) {
|
|
|
70
70
|
// reminder. The full no-verdict contract lives at system level
|
|
71
71
|
// (rules/agent/30-explorer.md).
|
|
72
72
|
export function buildExplorerPrompt(query) {
|
|
73
|
-
return `<query>${escapeXml(query)}</query>\nReminder:
|
|
73
|
+
return `<query>${escapeXml(query)}</query>\nReminder: coordinates only — WHERE, never WHY. Finish within 5 tool turns, ideally 1. Turn 1 MUST be one maximal batch: fire EVERY candidate lookup at once (grep with all token variants + code_graph + find/glob in the same turn); never one tool at a time. Answer straight from search output — grep/code_graph hits already carry path:line; never spend a turn reading to confirm, polish, or understand. The FIRST credible anchor ends the search: STOP and answer in that same turn, marking weak anchors with ?. By turn 5 answer with best-so-far or EXPLORATION_FAILED. Output only anchor lines formatted as path:line — symbol/name — short reason, or EXPLORATION_FAILED. No preamble, bullets, numbering, headings, summary, code quotes, analysis, verdicts, ratings, or recommendations.`;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function normalizeExploreQueries(rawQuery) {
|
|
@@ -355,16 +355,15 @@ async function runExploreSync(args = {}, ctx = {}) {
|
|
|
355
355
|
const presetName = (route && typeof route === 'object')
|
|
356
356
|
? `${clean(route.provider)}/${clean(route.model)}`
|
|
357
357
|
: (route || '');
|
|
358
|
-
// Explorer self-manages its turn budget: the
|
|
358
|
+
// Explorer self-manages its turn budget: the 5-turn budget lives ONLY in
|
|
359
359
|
// explorer-scoped surfaces (rules/agent/30-explorer.md + per-query prompt),
|
|
360
|
-
// never
|
|
361
|
-
//
|
|
362
|
-
//
|
|
360
|
+
// never as a loop hard cap — no maxLoopIterations here by design. The
|
|
361
|
+
// wall-clock hard timeout (EXPLORE_COMPUTE_HARD_TIMEOUT_MS) remains the
|
|
362
|
+
// only runaway guard.
|
|
363
363
|
const llm = makeAgentDispatch({
|
|
364
364
|
agent: 'explorer',
|
|
365
365
|
cwd: resolvedCwd,
|
|
366
366
|
brief: true,
|
|
367
|
-
maxLoopIterations: 12,
|
|
368
367
|
parentSessionId: ctx.callerSessionId || null,
|
|
369
368
|
clientHostPid: ctx.clientHostPid || null,
|
|
370
369
|
config,
|