mixdog 0.9.35 → 0.9.37
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 +3 -2
- package/scripts/abort-recovery-test.mjs +118 -0
- package/scripts/compact-smoke.mjs +7 -7
- package/scripts/explore-bench-tmp.mjs +19 -0
- package/scripts/explore-bench.mjs +36 -6
- package/scripts/explore-prompt-policy-test.mjs +27 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
- package/scripts/openai-ws-early-settle-test.mjs +176 -0
- package/scripts/output-style-smoke.mjs +17 -17
- package/scripts/provider-toolcall-test.mjs +333 -14
- package/scripts/recall-bench-cases.json +3 -3
- package/scripts/recall-bench.mjs +1 -1
- package/scripts/recall-quality-cases.json +4 -4
- package/scripts/recall-usecase-cases.json +2 -2
- package/scripts/session-bench.mjs +13 -13
- package/scripts/steering-drain-buckets-test.mjs +72 -11
- package/scripts/submit-commandbusy-race-test.mjs +114 -0
- package/scripts/tool-smoke.mjs +83 -10
- package/src/app.mjs +2 -1
- package/src/defaults/cycle3-review-prompt.md +9 -0
- package/src/defaults/skills/setup/SKILL.md +93 -293
- package/src/lib/rules-builder.cjs +3 -2
- package/src/output-styles/default.md +2 -2
- package/src/output-styles/extreme-minimal.md +1 -1
- package/src/output-styles/minimal.md +1 -1
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/30-explorer.md +15 -7
- package/src/rules/lead/01-general.md +4 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
- package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +26 -17
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +18 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
- package/src/runtime/channels/lib/worker-main.mjs +5 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
- package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
- package/src/runtime/memory/lib/recall-format.mjs +3 -3
- package/src/runtime/shared/child-spawn-gate.mjs +15 -0
- package/src/runtime/shared/config.mjs +98 -12
- package/src/runtime/shared/process-listener-headroom.mjs +12 -0
- package/src/session-runtime/cwd-plugins.mjs +6 -3
- package/src/session-runtime/model-route-api.mjs +50 -2
- package/src/session-runtime/provider-auth-api.mjs +8 -1
- package/src/session-runtime/resource-api.mjs +22 -0
- package/src/session-runtime/runtime-core.mjs +13 -2
- package/src/session-runtime/settings-api.mjs +11 -2
- package/src/standalone/agent-tool.mjs +15 -9
- package/src/standalone/explore-tool.mjs +4 -1
- package/src/tui/App.jsx +6 -5
- package/src/tui/app/transcript-window.mjs +60 -10
- package/src/tui/app/use-transcript-window.mjs +2 -1
- package/src/tui/components/ContextPanel.jsx +4 -1
- package/src/tui/components/ToolExecution.jsx +15 -12
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
- package/src/tui/dist/index.mjs +473 -155
- package/src/tui/engine/agent-job-feed.mjs +26 -6
- package/src/tui/engine/context-state.mjs +13 -4
- package/src/tui/engine/notification-plan.mjs +3 -3
- package/src/tui/engine/render-timing.mjs +104 -8
- package/src/tui/engine/session-api.mjs +58 -3
- package/src/tui/engine/session-flow.mjs +78 -28
- package/src/tui/engine/tool-card-results.mjs +28 -13
- package/src/tui/engine/turn.mjs +232 -156
- package/src/tui/engine.mjs +17 -8
- package/src/tui/index.jsx +10 -1
- package/src/workflows/default/WORKFLOW.md +8 -4
- package/src/workflows/solo/WORKFLOW.md +8 -4
|
@@ -16,7 +16,7 @@ import { readServicePort, markServiceUnreachable, isConnRefuseError } from '../.
|
|
|
16
16
|
const AUTO_DETECT_PORTS = {
|
|
17
17
|
'mixdog-memory': { discovery: 'memory', dir: 'mixdog', file: 'active-instance.json', portField: 'memory_port', endpoint: '/mcp' },
|
|
18
18
|
};
|
|
19
|
-
const DEFAULT_MCP_CALL_TIMEOUT_MS =
|
|
19
|
+
const DEFAULT_MCP_CALL_TIMEOUT_MS = 120000;
|
|
20
20
|
// Per-server STARTUP handshake budget (connect + listTools). Codex parity: 10s.
|
|
21
21
|
export const DEFAULT_MCP_STARTUP_TIMEOUT_MS = 10000;
|
|
22
22
|
// --- State ---
|
|
@@ -227,12 +227,12 @@ export async function executeMcpTool(name, args) {
|
|
|
227
227
|
return capMcpOutput(text);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
// MCP per-tool-call timeout.
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
// per-server timeoutMs/callTimeoutMs config value
|
|
234
|
-
// transport so the next dispatch reconnects fresh, but
|
|
235
|
-
// timed-out call automatically.
|
|
230
|
+
// MCP per-tool-call timeout. Default 2min: a hung/unresponsive MCP server
|
|
231
|
+
// (e.g. a busy editor) must not stall a tool call indefinitely. Genuinely
|
|
232
|
+
// long-running tools can raise/disable it via MIXDOG_MCP_CALL_TIMEOUT_MS or a
|
|
233
|
+
// per-server timeoutMs/callTimeoutMs config value (0/off/none/false disables).
|
|
234
|
+
// On expiry we close the transport so the next dispatch reconnects fresh, but
|
|
235
|
+
// we do not retry the timed-out call automatically (avoids side-effect dupes).
|
|
236
236
|
export function resolveMcpCallTimeoutMs(cfg = {}, env = process.env) {
|
|
237
237
|
const raw = cfg?.timeoutMs ?? cfg?.timeout_ms ?? cfg?.callTimeoutMs ?? cfg?.call_timeout_ms
|
|
238
238
|
?? env?.MIXDOG_MCP_CALL_TIMEOUT_MS;
|
|
@@ -373,15 +373,15 @@ function toAnthropicMessages(messages) {
|
|
|
373
373
|
|
|
374
374
|
if (m.role === 'tool') {
|
|
375
375
|
const last = result[result.length - 1];
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
376
|
+
// Do not replay native deferred-loader `tool_reference` blocks from
|
|
377
|
+
// prior turns. They are tied to the exact Anthropic deferred-tool
|
|
378
|
+
// request that produced them; after /model or provider switches the
|
|
379
|
+
// new request may not carry the matching deferred tool surface and
|
|
380
|
+
// Anthropic rejects the history as a request validation error.
|
|
379
381
|
const block = {
|
|
380
382
|
type: 'tool_result',
|
|
381
383
|
tool_use_id: m.toolCallId || '',
|
|
382
|
-
content:
|
|
383
|
-
? refs.map((name) => ({ type: 'tool_reference', tool_name: name }))
|
|
384
|
-
: normalizeContentForAnthropic(m.content),
|
|
384
|
+
content: normalizeContentForAnthropic(m.content),
|
|
385
385
|
};
|
|
386
386
|
if (last?.role === 'user' && Array.isArray(last.content)) {
|
|
387
387
|
last.content.push(block);
|
|
@@ -376,15 +376,15 @@ function toAnthropicMessages(messages) {
|
|
|
376
376
|
}
|
|
377
377
|
if (m.role === 'tool') {
|
|
378
378
|
const last = result[result.length - 1];
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
379
|
+
// Do not replay native deferred-loader `tool_reference` blocks from
|
|
380
|
+
// prior turns. They are tied to the exact Anthropic deferred-tool
|
|
381
|
+
// request that produced them; after /model or provider switches the
|
|
382
|
+
// new request may not carry the matching deferred tool surface and
|
|
383
|
+
// Anthropic rejects the history as a request validation error.
|
|
382
384
|
const block = {
|
|
383
385
|
type: 'tool_result',
|
|
384
386
|
tool_use_id: m.toolCallId || '',
|
|
385
|
-
content:
|
|
386
|
-
? refs.map((name) => ({ type: 'tool_reference', tool_name: name }))
|
|
387
|
-
: normalizeContentForAnthropic(m.content),
|
|
387
|
+
content: normalizeContentForAnthropic(m.content),
|
|
388
388
|
};
|
|
389
389
|
if (last?.role === 'user' && Array.isArray(last.content)) {
|
|
390
390
|
last.content.push(block);
|
|
@@ -15,31 +15,61 @@ function pushUnique(list, value) {
|
|
|
15
15
|
if (!list.includes(value)) list.push(value);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Short-TTL memo. Under simultaneous multi-agent launch, buildDefaultConfig
|
|
19
|
+
// and registry lazy-init call these probes in bursts; each probe does
|
|
20
|
+
// synchronous existsSync + readFileSync disk work. Cache the boolean per key
|
|
21
|
+
// for a brief window so one launch burst shares a single read instead of N.
|
|
22
|
+
// Kept short so the registry's "re-probe on each getProvider miss" self-heal
|
|
23
|
+
// (a credential file appearing/changing) is delayed by at most PROBE_TTL_MS.
|
|
24
|
+
const PROBE_TTL_MS = 3000;
|
|
25
|
+
const _probeCache = new Map();
|
|
26
|
+
function memoProbe(key, compute) {
|
|
27
|
+
const hit = _probeCache.get(key);
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (hit && now - hit.ts < PROBE_TTL_MS) return hit.value;
|
|
30
|
+
const value = compute();
|
|
31
|
+
_probeCache.set(key, { ts: now, value });
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
export function hasAnthropicOAuthCredentials() {
|
|
36
|
+
return memoProbe('anthropic-oauth', () => {
|
|
19
37
|
const paths = [];
|
|
38
|
+
const candidates = [];
|
|
20
39
|
pushUnique(paths, process.env.ANTHROPIC_OAUTH_CREDENTIALS_PATH);
|
|
21
40
|
pushUnique(paths, ANTHROPIC_DEFAULT_CREDENTIALS_PATH);
|
|
22
41
|
for (const path of paths) {
|
|
23
42
|
const raw = readJsonIfExists(path);
|
|
24
43
|
const oauth = raw?.claudeAiOauth;
|
|
25
|
-
if (oauth?.accessToken
|
|
26
|
-
|
|
44
|
+
if (oauth?.accessToken) {
|
|
45
|
+
candidates.push({
|
|
46
|
+
accessToken: oauth.accessToken,
|
|
47
|
+
expiresAt: Number(oauth.expiresAt ?? oauth.expires_at) || 0,
|
|
48
|
+
scopes: Array.isArray(oauth.scopes) ? oauth.scopes : [],
|
|
49
|
+
});
|
|
27
50
|
}
|
|
28
51
|
}
|
|
29
|
-
return false;
|
|
52
|
+
if (!candidates.length) return false;
|
|
53
|
+
candidates.sort((a, b) => (Number(b.expiresAt) || 0) - (Number(a.expiresAt) || 0));
|
|
54
|
+
const chosen = candidates[0];
|
|
55
|
+
return !!(chosen.accessToken && Array.isArray(chosen.scopes) && chosen.scopes.includes('user:inference'));
|
|
56
|
+
});
|
|
30
57
|
}
|
|
31
58
|
|
|
32
59
|
export function hasOpenAIOAuthCredentials() {
|
|
60
|
+
return memoProbe('openai-oauth', () => {
|
|
33
61
|
const paths = [join(resolvePluginData(), 'openai-oauth.json')];
|
|
34
62
|
for (const path of paths) {
|
|
35
63
|
const raw = readJsonIfExists(path);
|
|
36
|
-
|
|
37
|
-
if (tokens?.access_token && tokens?.refresh_token) return true;
|
|
64
|
+
if (raw?.access_token && raw?.refresh_token) return true;
|
|
38
65
|
}
|
|
39
66
|
return false;
|
|
67
|
+
});
|
|
40
68
|
}
|
|
41
69
|
|
|
42
70
|
export function hasGrokOAuthCredentials() {
|
|
71
|
+
return memoProbe('grok-oauth', () => {
|
|
43
72
|
const own = readJsonIfExists(join(resolvePluginData(), 'grok-oauth.json'));
|
|
44
73
|
return !!(own?.access_token && own?.refresh_token);
|
|
74
|
+
});
|
|
45
75
|
}
|
|
@@ -10,6 +10,7 @@ import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
|
10
10
|
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
11
11
|
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
12
12
|
import { randomBytes } from 'crypto';
|
|
13
|
+
import { createActiveToolItemTracker } from './tool-stream-state.mjs';
|
|
13
14
|
|
|
14
15
|
// Synthesize a native-shaped OpenAI tool call from a recovered leaked call.
|
|
15
16
|
// Matches the `call_...` id scheme the native Responses/Chat paths use so the
|
|
@@ -554,14 +555,17 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
554
555
|
name: event.item.name || '',
|
|
555
556
|
callId: event.item.call_id || '',
|
|
556
557
|
});
|
|
558
|
+
state.toolTracker?.mark(event.item);
|
|
557
559
|
state.toolInFlight = true;
|
|
558
560
|
} else if (event.item?.type === 'custom_tool_call') {
|
|
561
|
+
state.toolTracker?.mark(event.item);
|
|
559
562
|
state.toolInFlight = true;
|
|
560
563
|
} else if (event.item?.type === 'tool_search_call') {
|
|
561
564
|
// Mark tool_search in-flight at item-added time, same as
|
|
562
565
|
// function_call/custom_tool_call above, so the stall-recovery
|
|
563
566
|
// pendingToolUse gate never drops a mid-flight tool_search
|
|
564
567
|
// before response.output_item.done pushes it.
|
|
568
|
+
state.toolTracker?.mark(event.item);
|
|
565
569
|
state.toolInFlight = true;
|
|
566
570
|
}
|
|
567
571
|
try { onStreamDelta?.(); } catch {}
|
|
@@ -569,6 +573,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
569
573
|
case 'response.function_call_arguments.delta':
|
|
570
574
|
// A tool call's args are streaming — mark tool work in-flight so a
|
|
571
575
|
// mid-args stall is NEVER accepted as a text-only partial-final.
|
|
576
|
+
state.toolTracker?.mark(null, event.item_id);
|
|
572
577
|
state.toolInFlight = true;
|
|
573
578
|
try { onStreamDelta?.(); } catch {}
|
|
574
579
|
break;
|
|
@@ -576,6 +581,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
576
581
|
// Custom-tool input streams before output_item.done records the call
|
|
577
582
|
// in pendingCalls; flag it so a mid-input stall gates out partial-
|
|
578
583
|
// final success (otherwise a tool-bearing turn looks text-only).
|
|
584
|
+
state.toolTracker?.mark(null, event.item_id);
|
|
579
585
|
state.toolInFlight = true;
|
|
580
586
|
try { onStreamDelta?.(); } catch {}
|
|
581
587
|
break;
|
|
@@ -616,10 +622,21 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
616
622
|
state.toolCalls.push(call);
|
|
617
623
|
emitCompatToolCallOnce(state, call, onToolCall);
|
|
618
624
|
}
|
|
625
|
+
// Drop the resolved function item from pendingCalls before
|
|
626
|
+
// recomputing toolInFlight — otherwise a completed call keeps
|
|
627
|
+
// pendingCalls.size > 0 and the latch never clears, so a later
|
|
628
|
+
// text-only stall stays wrongly gated as tool-bearing.
|
|
629
|
+
if (itemId) state.pendingCalls.delete(itemId);
|
|
630
|
+
state.toolTracker?.clear(item, itemId);
|
|
631
|
+
state.toolInFlight = state.pendingCalls.size > 0 || (state.toolTracker ? state.toolTracker.items.size > 0 : false);
|
|
619
632
|
} else if (item.type === 'tool_search_call') {
|
|
620
633
|
pushToolSearchCall(item);
|
|
634
|
+
state.toolTracker?.clear(item, item.id || '');
|
|
635
|
+
state.toolInFlight = state.pendingCalls.size > 0 || (state.toolTracker ? state.toolTracker.items.size > 0 : false);
|
|
621
636
|
} else if (item.type === 'custom_tool_call') {
|
|
622
637
|
pushCustomToolCall(item);
|
|
638
|
+
state.toolTracker?.clear(item, item.id || '');
|
|
639
|
+
state.toolInFlight = state.pendingCalls.size > 0 || (state.toolTracker ? state.toolTracker.items.size > 0 : false);
|
|
623
640
|
}
|
|
624
641
|
try { onStreamDelta?.(); } catch {}
|
|
625
642
|
break;
|
|
@@ -746,6 +763,14 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
746
763
|
pendingCalls: new Map(),
|
|
747
764
|
emittedToolCallKeys: new Set(),
|
|
748
765
|
emittedToolCall: false,
|
|
766
|
+
// Active tool-item / alias tracking shared with the WS + HTTP-SSE
|
|
767
|
+
// Responses streams (tool-stream-state.mjs): mark on output_item.added /
|
|
768
|
+
// arg-input deltas, clear on output_item.done. Unions id/call_id/item_id
|
|
769
|
+
// aliases so a mark under one key and a clear under another resolve to
|
|
770
|
+
// the same item — closes the custom-tool-input in-flight gap that a bare
|
|
771
|
+
// boolean toolInFlight latch could not (a mid-input stall now gates out
|
|
772
|
+
// text-only partial-final).
|
|
773
|
+
toolTracker: createActiveToolItemTracker(),
|
|
749
774
|
completed: false,
|
|
750
775
|
completedResponse: null,
|
|
751
776
|
sawOutput: false,
|
|
@@ -823,6 +848,7 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
823
848
|
|| leakedCalls.length > 0
|
|
824
849
|
|| (state.pendingCalls && state.pendingCalls.size > 0)
|
|
825
850
|
|| (Array.isArray(state.toolCalls) && state.toolCalls.length > 0)
|
|
851
|
+
|| (state.toolTracker && state.toolTracker.items.size > 0)
|
|
826
852
|
|| state.toolInFlight === true;
|
|
827
853
|
err.partialModel = state.model || undefined;
|
|
828
854
|
} catch { /* best-effort */ }
|
|
@@ -849,6 +875,7 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
849
875
|
|| leakedCalls.length > 0
|
|
850
876
|
|| (state.pendingCalls && state.pendingCalls.size > 0)
|
|
851
877
|
|| (Array.isArray(state.toolCalls) && state.toolCalls.length > 0)
|
|
878
|
+
|| (state.toolTracker && state.toolTracker.items.size > 0)
|
|
852
879
|
|| state.toolInFlight === true;
|
|
853
880
|
err.partialModel = state.model || undefined;
|
|
854
881
|
} catch { /* best-effort */ }
|
|
@@ -105,11 +105,26 @@ export function toOpenAITools(tools) {
|
|
|
105
105
|
}));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
function functionToolFromSessionTool(t, name = t?.name) {
|
|
109
|
+
return {
|
|
110
|
+
type: 'function',
|
|
111
|
+
name,
|
|
112
|
+
description: t.description,
|
|
113
|
+
parameters: t.inputSchema || { type: 'object', additionalProperties: true },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function toResponsesTools(tools, options = {}) {
|
|
118
|
+
const provider = String(options?.provider || '').toLowerCase();
|
|
119
|
+
const allowNativeToolSearch = options?.nativeToolSearch === true
|
|
120
|
+
|| (options?.nativeToolSearch !== false && provider !== 'xai');
|
|
109
121
|
return tools.map((t) => {
|
|
110
122
|
// load_tool advertises as the OpenAI-native `tool_search` wire type
|
|
111
|
-
// (legacy 'tool_search' name still accepted for back-compat).
|
|
123
|
+
// (legacy 'tool_search' name still accepted for back-compat). xAI/Grok
|
|
124
|
+
// Responses rejects that OpenAI-only variant ("unknown variant
|
|
125
|
+
// 'tool_search'"), so serialize it as an ordinary function there.
|
|
112
126
|
if (t?.name === 'load_tool' || t?.name === 'tool_search') {
|
|
127
|
+
if (!allowNativeToolSearch) return functionToolFromSessionTool(t, 'load_tool');
|
|
113
128
|
return {
|
|
114
129
|
type: 'tool_search',
|
|
115
130
|
execution: 'client',
|
|
@@ -122,12 +137,7 @@ export function toResponsesTools(tools) {
|
|
|
122
137
|
// tools (e.g. apply_patch) as ordinary function tools instead. Grammar
|
|
123
138
|
// tools may carry no usable inputSchema, so fall back to a permissive
|
|
124
139
|
// object schema so grok still registers a valid function tool.
|
|
125
|
-
return
|
|
126
|
-
type: 'function',
|
|
127
|
-
name: t.name,
|
|
128
|
-
description: t.description,
|
|
129
|
-
parameters: t.inputSchema || { type: 'object', additionalProperties: true },
|
|
130
|
-
};
|
|
140
|
+
return functionToolFromSessionTool(t);
|
|
131
141
|
});
|
|
132
142
|
}
|
|
133
143
|
|
|
@@ -271,18 +281,10 @@ export function collectCompatResponseSearchSources(response) {
|
|
|
271
281
|
|
|
272
282
|
export function toResponsesInputMessage(m, pendingToolMedia = null, customToolCallNameById = null) {
|
|
273
283
|
if (m.role === 'tool') {
|
|
274
|
-
if (Array.isArray(m.nativeToolSearch?.openaiTools)) {
|
|
275
|
-
return {
|
|
276
|
-
type: 'tool_search_output',
|
|
277
|
-
call_id: m.toolCallId || '',
|
|
278
|
-
status: 'completed',
|
|
279
|
-
execution: 'client',
|
|
280
|
-
tools: m.nativeToolSearch.openaiTools,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
284
|
const { output, mediaContent } = splitToolContentForOpenAIResponses(m.content);
|
|
284
285
|
// xai path: never emit `custom_tool_call_output` (the `custom` variant
|
|
285
|
-
// is rejected by grok). Replay prior tool outputs
|
|
286
|
+
// is rejected by grok). Replay prior tool outputs — including old
|
|
287
|
+
// native tool_search outputs after /model switches — as the standard
|
|
286
288
|
// `function_call_output` item regardless of original native type.
|
|
287
289
|
const item = {
|
|
288
290
|
type: 'function_call_output',
|
|
@@ -296,25 +298,16 @@ export function toResponsesInputMessage(m, pendingToolMedia = null, customToolCa
|
|
|
296
298
|
const items = [];
|
|
297
299
|
if (m.content) items.push({ role: 'assistant', content: normalizeContentForOpenAIResponses(m.content, { role: 'assistant' }) });
|
|
298
300
|
for (const tc of m.toolCalls) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
// variant). tc.arguments already holds the recovered object
|
|
310
|
-
// form, so the same stringify path as regular calls applies.
|
|
311
|
-
items.push({
|
|
312
|
-
type: 'function_call',
|
|
313
|
-
call_id: tc.id,
|
|
314
|
-
name: tc.name,
|
|
315
|
-
arguments: JSON.stringify(tc.arguments || {}),
|
|
316
|
-
});
|
|
317
|
-
}
|
|
301
|
+
// xAI/Grok rejects OpenAI-only Responses variants such as
|
|
302
|
+
// `custom_tool_call` and `tool_search_call`, including when they
|
|
303
|
+
// appear in replayed history after a model switch. Replay all prior
|
|
304
|
+
// client tools as plain function calls.
|
|
305
|
+
items.push({
|
|
306
|
+
type: 'function_call',
|
|
307
|
+
call_id: tc.id,
|
|
308
|
+
name: tc.name === 'tool_search' ? 'load_tool' : tc.name,
|
|
309
|
+
arguments: JSON.stringify(tc.arguments || {}),
|
|
310
|
+
});
|
|
318
311
|
}
|
|
319
312
|
return items;
|
|
320
313
|
}
|
|
@@ -351,6 +344,7 @@ export function toXaiResponsesInput(messages, providerState, options = {}) {
|
|
|
351
344
|
startIndex = Math.max(0, Math.min(seen, messages.length));
|
|
352
345
|
if (messages[startIndex]?.role === 'assistant') startIndex += 1;
|
|
353
346
|
}
|
|
347
|
+
const dropToolHistory = !previousResponseId && (resetReason !== null || !state?.previousResponseId);
|
|
354
348
|
const input = [];
|
|
355
349
|
const pendingToolMedia = [];
|
|
356
350
|
const customToolCallNameById = new Map();
|
|
@@ -361,6 +355,11 @@ export function toXaiResponsesInput(messages, providerState, options = {}) {
|
|
|
361
355
|
for (const m of messages.slice(startIndex)) {
|
|
362
356
|
if (!includeSystem && m.role === 'system') continue;
|
|
363
357
|
if (m.role !== 'tool') flushToolMedia();
|
|
358
|
+
if (dropToolHistory && m.role === 'tool') continue;
|
|
359
|
+
if (dropToolHistory && m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
360
|
+
if (m.content) input.push({ role: 'assistant', content: normalizeContentForOpenAIResponses(m.content, { role: 'assistant' }) });
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
364
363
|
const converted = toResponsesInputMessage(m, pendingToolMedia, customToolCallNameById);
|
|
365
364
|
if (Array.isArray(converted)) input.push(...converted);
|
|
366
365
|
else input.push(converted);
|
|
@@ -467,7 +467,7 @@ export class OpenAICompatProvider {
|
|
|
467
467
|
};
|
|
468
468
|
if (previousResponseId) params.previous_response_id = previousResponseId;
|
|
469
469
|
const nativeTools = nativeResponsesTools(opts);
|
|
470
|
-
if (tools?.length || nativeTools.length) params.tools = [...nativeTools, ...toResponsesTools(tools || [])];
|
|
470
|
+
if (tools?.length || nativeTools.length) params.tools = [...nativeTools, ...toResponsesTools(tools || [], { provider: 'xai' })];
|
|
471
471
|
// SSE transport: report 'requesting' until the stream opens, then
|
|
472
472
|
// per-chunk onStreamDelta feeds the agent stall watchdog.
|
|
473
473
|
try { opts.onStageChange?.('requesting'); } catch { /* heartbeat best-effort */ }
|
|
@@ -664,7 +664,7 @@ export class OpenAICompatProvider {
|
|
|
664
664
|
// first response already anchors instructions for the continuation.
|
|
665
665
|
else if (instructions) params.instructions = instructions;
|
|
666
666
|
const nativeTools = nativeResponsesTools(opts);
|
|
667
|
-
if (tools?.length || nativeTools.length) params.tools = [...nativeTools, ...toResponsesTools(tools || [])];
|
|
667
|
+
if (tools?.length || nativeTools.length) params.tools = [...nativeTools, ...toResponsesTools(tools || [], { provider: 'xai' })];
|
|
668
668
|
const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
|
|
669
669
|
?? opts.effort
|
|
670
670
|
?? this.config?.reasoningEffort
|
|
@@ -26,6 +26,7 @@ import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
|
26
26
|
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
27
27
|
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
28
28
|
import { CODEX_OAUTH_ORIGINATOR, CODEX_RESPONSES_URL, _displayCodexModel } from './openai-oauth.mjs';
|
|
29
|
+
import { createActiveToolItemTracker } from './tool-stream-state.mjs';
|
|
29
30
|
|
|
30
31
|
// Public OpenAI Responses API endpoint for the api-key `openai` provider.
|
|
31
32
|
// The openai-direct WS transport hits the same origin (openai-ws-pool
|
|
@@ -297,7 +298,15 @@ export async function sendViaHttpSse({
|
|
|
297
298
|
try {
|
|
298
299
|
_streamAbortReason.partialContent = content;
|
|
299
300
|
_streamAbortReason.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
300
|
-
|
|
301
|
+
// Shared active tool-item tracker (tool-stream-state.mjs) closes
|
|
302
|
+
// the custom-tool gap: a stall mid custom_tool_call_input.delta
|
|
303
|
+
// never lands in pendingCalls, so without activeToolItems/_toolInFlight
|
|
304
|
+
// a tool-bearing turn would look text-only and be wrongly accepted
|
|
305
|
+
// as a partial-final.
|
|
306
|
+
_streamAbortReason.pendingToolUse = pendingCalls.size > 0
|
|
307
|
+
|| emittedToolCallIds.size > 0
|
|
308
|
+
|| activeToolItems.size > 0
|
|
309
|
+
|| _toolInFlight === true;
|
|
301
310
|
_streamAbortReason.partialModel = model || undefined;
|
|
302
311
|
} catch { /* best-effort enrichment */ }
|
|
303
312
|
try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
|
|
@@ -313,6 +322,15 @@ export async function sendViaHttpSse({
|
|
|
313
322
|
let ttftMs = null;
|
|
314
323
|
const toolCalls = [];
|
|
315
324
|
const pendingCalls = new Map();
|
|
325
|
+
// Active tool-item / alias tracking shared with the WS + compat Responses
|
|
326
|
+
// streams (tool-stream-state.mjs). Mark on output_item.added / arg-input
|
|
327
|
+
// deltas, clear on output_item.done; _toolInFlight latches tool work the
|
|
328
|
+
// moment a call's input starts streaming (before it lands in pendingCalls).
|
|
329
|
+
const _toolTracker = createActiveToolItemTracker();
|
|
330
|
+
const activeToolItems = _toolTracker.items;
|
|
331
|
+
const markActiveToolItem = _toolTracker.mark;
|
|
332
|
+
const clearActiveToolItem = _toolTracker.clear;
|
|
333
|
+
let _toolInFlight = false;
|
|
316
334
|
const reasoningItems = [];
|
|
317
335
|
const citations = [];
|
|
318
336
|
const citationKeys = new Set();
|
|
@@ -486,10 +504,12 @@ export async function sendViaHttpSse({
|
|
|
486
504
|
break;
|
|
487
505
|
case 'response.output_item.added':
|
|
488
506
|
if (event.item?.type === 'function_call') {
|
|
507
|
+
markActiveToolItem(event.item);
|
|
489
508
|
pendingCalls.set(event.item.id || '', {
|
|
490
509
|
name: event.item.name || '',
|
|
491
510
|
callId: event.item.call_id || '',
|
|
492
511
|
});
|
|
512
|
+
_toolInFlight = true;
|
|
493
513
|
} else if (event.item?.type === 'tool_search_call') {
|
|
494
514
|
// Mark tool_search as in-flight the moment the item is
|
|
495
515
|
// added, mirroring function_call above, so the semantic
|
|
@@ -506,9 +526,21 @@ export async function sendViaHttpSse({
|
|
|
506
526
|
kind: 'tool_search',
|
|
507
527
|
});
|
|
508
528
|
}
|
|
529
|
+
markActiveToolItem(event.item);
|
|
530
|
+
_toolInFlight = true;
|
|
531
|
+
} else if (event.item?.type === 'custom_tool_call') {
|
|
532
|
+
// Custom tool calls surface no pendingCalls entry, so mark
|
|
533
|
+
// the item active at added-time (mirroring function_call /
|
|
534
|
+
// tool_search_call above). The later custom_tool_call_input.delta
|
|
535
|
+
// still marks too, but a stall between added and the first
|
|
536
|
+
// input delta must already read as pendingToolUse.
|
|
537
|
+
markActiveToolItem(event.item);
|
|
538
|
+
_toolInFlight = true;
|
|
509
539
|
}
|
|
510
540
|
break;
|
|
511
541
|
case 'response.function_call_arguments.delta':
|
|
542
|
+
markActiveToolItem(null, event.item_id);
|
|
543
|
+
_toolInFlight = true;
|
|
512
544
|
meaningful();
|
|
513
545
|
break;
|
|
514
546
|
case 'response.function_call_arguments.done': {
|
|
@@ -530,6 +562,8 @@ export async function sendViaHttpSse({
|
|
|
530
562
|
break;
|
|
531
563
|
}
|
|
532
564
|
case 'response.custom_tool_call_input.delta':
|
|
565
|
+
markActiveToolItem(null, event.item_id);
|
|
566
|
+
_toolInFlight = true;
|
|
533
567
|
meaningful();
|
|
534
568
|
break;
|
|
535
569
|
case 'response.output_item.done': {
|
|
@@ -546,11 +580,23 @@ export async function sendViaHttpSse({
|
|
|
546
580
|
emitToolCall(tc);
|
|
547
581
|
}
|
|
548
582
|
}
|
|
583
|
+
// Drop the resolved function item from pendingCalls before
|
|
584
|
+
// recomputing _toolInFlight (mirrors tool_search_call below
|
|
585
|
+
// and the compat path) — otherwise a completed call keeps
|
|
586
|
+
// pendingCalls.size > 0 and the latch never clears, so a
|
|
587
|
+
// later max-output cutoff is misread as a tool in flight.
|
|
588
|
+
pendingCalls.delete(item.id || '');
|
|
589
|
+
clearActiveToolItem(item, item.id || '');
|
|
590
|
+
_toolInFlight = pendingCalls.size > 0 || activeToolItems.size > 0;
|
|
549
591
|
} else if (item.type === 'tool_search_call') {
|
|
550
592
|
pendingCalls.delete(item.id || '');
|
|
551
593
|
pushToolSearchCall(item);
|
|
594
|
+
clearActiveToolItem(item, item.id || '');
|
|
595
|
+
_toolInFlight = pendingCalls.size > 0 || activeToolItems.size > 0;
|
|
552
596
|
} else if (item.type === 'custom_tool_call') {
|
|
553
597
|
pushCustomToolCall(item);
|
|
598
|
+
clearActiveToolItem(item, item.id || '');
|
|
599
|
+
_toolInFlight = pendingCalls.size > 0 || activeToolItems.size > 0;
|
|
554
600
|
meaningful();
|
|
555
601
|
}
|
|
556
602
|
break;
|
|
@@ -636,6 +682,20 @@ export async function sendViaHttpSse({
|
|
|
636
682
|
} else if (event.response.status === 'incomplete') {
|
|
637
683
|
const reason = _incompleteReasonFromEvent(event);
|
|
638
684
|
if (_isMaxOutputIncompleteReason(reason)) {
|
|
685
|
+
// Max-output cutoff with a function/custom/tool_search
|
|
686
|
+
// still in flight means the tool arguments were
|
|
687
|
+
// truncated — do NOT mark a clean completion (mirrors
|
|
688
|
+
// the compat Responses path), or partial args surface as
|
|
689
|
+
// a successful tool call. Throw a stream-stalled
|
|
690
|
+
// pendingToolUse error so the loop gates/retries.
|
|
691
|
+
if (pendingCalls.size > 0 || activeToolItems.size > 0 || _toolInFlight === true) {
|
|
692
|
+
const err = _stampToolSafety(new Error('OpenAI OAuth HTTP fallback response.done incomplete (max_output_tokens) with tool call in flight'));
|
|
693
|
+
err.streamStalled = true;
|
|
694
|
+
err.pendingToolUse = true;
|
|
695
|
+
err.partialContent = content;
|
|
696
|
+
err.partialModel = model || undefined;
|
|
697
|
+
throw err;
|
|
698
|
+
}
|
|
639
699
|
completed = true;
|
|
640
700
|
stopReason = 'length';
|
|
641
701
|
break;
|
|
@@ -652,6 +712,17 @@ export async function sendViaHttpSse({
|
|
|
652
712
|
case 'response.incomplete': {
|
|
653
713
|
const reason = _incompleteReasonFromEvent(event);
|
|
654
714
|
if (_isMaxOutputIncompleteReason(reason)) {
|
|
715
|
+
// See response.done incomplete above: a max-output cutoff
|
|
716
|
+
// while a tool is in flight is a truncated tool call, not a
|
|
717
|
+
// clean length completion.
|
|
718
|
+
if (pendingCalls.size > 0 || activeToolItems.size > 0 || _toolInFlight === true) {
|
|
719
|
+
const err = _stampToolSafety(new Error('OpenAI OAuth HTTP fallback response.incomplete (max_output_tokens) with tool call in flight'));
|
|
720
|
+
err.streamStalled = true;
|
|
721
|
+
err.pendingToolUse = true;
|
|
722
|
+
err.partialContent = content;
|
|
723
|
+
err.partialModel = model || undefined;
|
|
724
|
+
throw err;
|
|
725
|
+
}
|
|
655
726
|
completed = true;
|
|
656
727
|
stopReason = 'length';
|
|
657
728
|
break;
|
|
@@ -895,7 +895,26 @@ export async function sendViaWebSocket({
|
|
|
895
895
|
// Delta opt-in still chains via entry.lastResponseId above.
|
|
896
896
|
}
|
|
897
897
|
|
|
898
|
-
|
|
898
|
+
// Warmup writes the same prefix with generate:false, but the first
|
|
899
|
+
// real response must still be a FULL generating frame. Reusing the
|
|
900
|
+
// warmup response_id here causes _buildResponseCreateFrame() to drop
|
|
901
|
+
// `instructions` (previous_response_id frames cannot carry them) and
|
|
902
|
+
// can also reduce the frame input to [] when the warmup input matches.
|
|
903
|
+
// That made first-turn lead/system rules effectively disappear
|
|
904
|
+
// ("who are you?" answered as a generic OpenAI assistant). Keep the
|
|
905
|
+
// warmup state for cache/trace, but compute the main frame as cold.
|
|
906
|
+
const deltaEntry = warmupResult
|
|
907
|
+
? {
|
|
908
|
+
...entry,
|
|
909
|
+
lastResponseId: null,
|
|
910
|
+
lastRequestSansInput: null,
|
|
911
|
+
lastRequestInput: null,
|
|
912
|
+
lastResponseItems: null,
|
|
913
|
+
lastInputLen: 0,
|
|
914
|
+
lastInputPrefixHash: null,
|
|
915
|
+
}
|
|
916
|
+
: entry;
|
|
917
|
+
const delta = _computeDelta({ entry: deltaEntry, body: requestBody, traceProvider });
|
|
899
918
|
({ mode, frame } = delta);
|
|
900
919
|
deltaReason = delta.reason || null;
|
|
901
920
|
strippedResponseItems = delta.strippedResponseItems || 0;
|
|
@@ -1063,7 +1082,11 @@ export async function sendViaWebSocket({
|
|
|
1063
1082
|
// own, so retaining the anchor cannot corrupt the cache — it only adds
|
|
1064
1083
|
// a delta fast-path when the items DO match.
|
|
1065
1084
|
const keepResponseChain = !!result.responseId;
|
|
1066
|
-
|
|
1085
|
+
// Normally the socket is pooled for reuse. But an early tool-call settle
|
|
1086
|
+
// (result.closeSocket) means the stream resolved before
|
|
1087
|
+
// response.completed/done arrived: the server may still emit those as
|
|
1088
|
+
// orphan frames, so the socket must be discarded, not reused.
|
|
1089
|
+
const keepSocket = !result.closeSocket;
|
|
1067
1090
|
|
|
1068
1091
|
// Update cache state for the next iteration in this session. openai-oauth
|
|
1069
1092
|
// keeps the previous response anchor even when the model emitted tool
|
|
@@ -1300,7 +1323,7 @@ export async function sendViaWebSocket({
|
|
|
1300
1323
|
} catch {}
|
|
1301
1324
|
|
|
1302
1325
|
releaseWebSocket({ entry, poolKey, keep: keepSocket });
|
|
1303
|
-
const { responseId: _ignored, responseItems: _responseItemsIgnored, ...out } = result;
|
|
1326
|
+
const { responseId: _ignored, responseItems: _responseItemsIgnored, closeSocket: _closeSocketIgnored, ...out } = result;
|
|
1304
1327
|
if (includeResponseId && result.responseId) out.responseId = result.responseId;
|
|
1305
1328
|
if (warmupResult) {
|
|
1306
1329
|
try {
|
|
@@ -410,16 +410,6 @@ function convertMessagesToResponsesInput(messages, opts = {}) {
|
|
|
410
410
|
for (const m of messages) {
|
|
411
411
|
if (!m || m.role === 'system') continue;
|
|
412
412
|
if (m.role === 'tool') {
|
|
413
|
-
if (Array.isArray(m.nativeToolSearch?.openaiTools)) {
|
|
414
|
-
out.push({
|
|
415
|
-
type: 'tool_search_output',
|
|
416
|
-
call_id: m.toolCallId || '',
|
|
417
|
-
status: 'completed',
|
|
418
|
-
execution: 'client',
|
|
419
|
-
tools: m.nativeToolSearch.openaiTools,
|
|
420
|
-
});
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
413
|
const { output, mediaContent } = splitToolContentForOpenAIResponses(m.content);
|
|
424
414
|
if (customToolCallNameById.has(m.toolCallId || '')) {
|
|
425
415
|
out.push({
|
|
@@ -449,14 +439,7 @@ function convertMessagesToResponsesInput(messages, opts = {}) {
|
|
|
449
439
|
// reasoning in `input` triggers "Duplicate item".
|
|
450
440
|
if (m.content) out.push(wireMessage('assistant', normalizeContentForOpenAIResponses(m.content, { role: 'assistant' })));
|
|
451
441
|
for (const tc of m.toolCalls) {
|
|
452
|
-
if (tc
|
|
453
|
-
out.push({
|
|
454
|
-
type: 'tool_search_call',
|
|
455
|
-
call_id: tc.id,
|
|
456
|
-
execution: 'client',
|
|
457
|
-
arguments: tc.arguments || {},
|
|
458
|
-
});
|
|
459
|
-
} else if (isCustomToolCallRecord(tc)) {
|
|
442
|
+
if (isCustomToolCallRecord(tc)) {
|
|
460
443
|
if (tc.id) customToolCallNameById.set(tc.id, tc.name || '');
|
|
461
444
|
out.push({
|
|
462
445
|
type: 'custom_tool_call',
|
|
@@ -468,7 +451,7 @@ function convertMessagesToResponsesInput(messages, opts = {}) {
|
|
|
468
451
|
out.push({
|
|
469
452
|
type: 'function_call',
|
|
470
453
|
call_id: tc.id,
|
|
471
|
-
name: tc.name,
|
|
454
|
+
name: tc.name === 'tool_search' ? 'load_tool' : tc.name,
|
|
472
455
|
arguments: JSON.stringify(tc.arguments),
|
|
473
456
|
});
|
|
474
457
|
}
|
|
@@ -487,8 +470,8 @@ function convertMessagesToResponsesInput(messages, opts = {}) {
|
|
|
487
470
|
function toOpenAIResponsesTool(t) {
|
|
488
471
|
if (t?.name === 'load_tool' || t?.name === 'tool_search') {
|
|
489
472
|
return {
|
|
490
|
-
type: '
|
|
491
|
-
|
|
473
|
+
type: 'function',
|
|
474
|
+
name: 'load_tool',
|
|
492
475
|
description: t.description,
|
|
493
476
|
parameters: t.inputSchema,
|
|
494
477
|
};
|
|
@@ -923,13 +906,14 @@ export class OpenAIOAuthProvider {
|
|
|
923
906
|
useModel,
|
|
924
907
|
displayModel: _displayCodexModel,
|
|
925
908
|
forceFresh,
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
|
|
909
|
+
// Default refs-style recovery: keep using WS first. A transient
|
|
910
|
+
// first-byte / mid-stream stall closes the bad socket and retries on
|
|
911
|
+
// a fresh WS entry; only after the bounded WS retry budget is
|
|
912
|
+
// exhausted does openai-oauth fall back to HTTP/SSE. This preserves
|
|
913
|
+
// the hot WS/cache path for temporary blips while still preventing
|
|
914
|
+
// TUI-level hangs. Operators can opt into immediate HTTP fallback
|
|
915
|
+
// for diagnostics with MIXDOG_OPENAI_OAUTH_FAST_HTTP_FALLBACK=1.
|
|
916
|
+
fastFallback: httpFallbackEnabled && _envFlag('MIXDOG_OPENAI_OAUTH_FAST_HTTP_FALLBACK', false),
|
|
933
917
|
// codex-parity prewarm (generate:false full frame on a fresh
|
|
934
918
|
// socket, wire-verified 2026-07-03). DEFAULT ON: R19(off) vs
|
|
935
919
|
// R20(on) A/B shows prewarm removes ALL early-session zero-cache
|