mixdog 0.9.7 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -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
- Find code anchors fast. Batch independent lookups in turn 1: one `grep`
12
- with all literal strings/errors/identifier guesses, plus `code_graph`/`find`
13
- or `glob` when useful.
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
- Prefer user-visible strings and identifiers. `dist`/generated hits are leads,
16
- not answers: trace them back to source. On miss, derive new tokens from
17
- returned paths/names or widen scope; do not repeat synonyms. NEVER issue
18
- serial single-tool turns: every turn after the first must batch ALL remaining
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: 4 tool turns. The FIRST credible `path:line` ends the search — answer
22
- with what you have; more anchors are not more value. Spend remaining turns
23
- only while you have zero anchors, and buy coverage with wider batches, never
24
- with extra serial turns. On ANY iteration warning, answer immediately with
25
- best anchors or `EXPLORATION_FAILED`.
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', 'full_default'].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 deltaOptIn = ['1', 'true', 'yes', 'on'].includes(String(process.env.MIXDOG_OAI_WS_DELTA || '').trim().toLowerCase());
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: budget 4 tool turns batch every candidate lookup per turn, never one tool at a time. The moment you have credible anchors, STOP and answer; spend remaining turns only while you have zero anchors. 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.`;
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 4-turn budget lives ONLY in
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 in shared loop policy. The cap here is a runaway guard, unchanged
361
- // from the original value; B-round live tests showed hard targets
362
- // legitimately need ~10.
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,