mixdog 0.9.151 → 0.9.152
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 +110 -68
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +18 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +1 -0
- package/src/tui/session-local.mjs +15 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
writeXaiResponsesCacheTrace,
|
|
65
65
|
} from './openai-compat-xai.mjs';
|
|
66
66
|
import { envFlag as _envFlag } from './lib/env-utils.mjs';
|
|
67
|
+
import { createProviderReplay } from './lib/provider-replay.mjs';
|
|
67
68
|
|
|
68
69
|
const requireOpenAI = createRequire(import.meta.url);
|
|
69
70
|
let _OpenAI = null;
|
|
@@ -168,10 +169,33 @@ function normalizeReasoningEffort(value, allowed) {
|
|
|
168
169
|
return allowed.includes(effort) ? effort : null;
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
export function compatReportedCostUsd(providerName, usage) {
|
|
173
|
+
if (providerName === 'openrouter') {
|
|
174
|
+
const cost = Number(usage?.cost);
|
|
175
|
+
return Number.isFinite(cost) && cost >= 0 ? cost : undefined;
|
|
176
|
+
}
|
|
177
|
+
if (providerName === 'xai') {
|
|
178
|
+
const ticks = Number(usage?.cost_in_usd_ticks);
|
|
179
|
+
return Number.isFinite(ticks) && ticks >= 0
|
|
180
|
+
? Number((ticks * 1e-10).toFixed(8))
|
|
181
|
+
: undefined;
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
171
186
|
// Keep provider extensions isolated: fields accepted by one OpenAI-compatible
|
|
172
187
|
// backend are frequently rejected by another even when the core Chat schema is
|
|
173
188
|
// shared.
|
|
174
189
|
export function applyCompatProviderChatOptions(params, providerName, opts = {}, config = {}, modelInfo = null) {
|
|
190
|
+
if (providerName === 'openrouter') {
|
|
191
|
+
const effort = normalizeReasoningEffort(
|
|
192
|
+
opts.openRouterReasoningEffort ?? opts.effort ?? config?.reasoningEffort,
|
|
193
|
+
['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'],
|
|
194
|
+
);
|
|
195
|
+
if (effort === 'none') params.reasoning = { enabled: false };
|
|
196
|
+
else if (effort) params.reasoning = { enabled: true, effort };
|
|
197
|
+
return params;
|
|
198
|
+
}
|
|
175
199
|
if (providerName === 'xai') {
|
|
176
200
|
const reasoningEffort = normalizeXaiReasoningEffort(opts.xaiReasoningEffort
|
|
177
201
|
?? opts.effort
|
|
@@ -594,6 +618,9 @@ export class OpenAICompatProvider {
|
|
|
594
618
|
const reasoningContent = (capturesReasoningContent && typeof assembled.reasoningContent === 'string')
|
|
595
619
|
? assembled.reasoningContent
|
|
596
620
|
: null;
|
|
621
|
+
const reasoningDetails = this.name === 'openrouter' && Array.isArray(choice?.message?.reasoning_details)
|
|
622
|
+
? choice.message.reasoning_details
|
|
623
|
+
: null;
|
|
597
624
|
return {
|
|
598
625
|
content: assembled.content || '',
|
|
599
626
|
// Streamed chunks can omit `model`; fall back to the requested
|
|
@@ -609,6 +636,9 @@ export class OpenAICompatProvider {
|
|
|
609
636
|
// instead of silently treating a truncated answer as complete.
|
|
610
637
|
...(stopReason === 'length' && (assembled.content || '').length > 0 ? { truncated: true } : {}),
|
|
611
638
|
...(reasoningContent ? { reasoningContent } : {}),
|
|
639
|
+
...(reasoningDetails?.length ? {
|
|
640
|
+
providerMetadata: { openrouter: { reasoning_details: reasoningDetails } },
|
|
641
|
+
} : {}),
|
|
612
642
|
usage: response.usage ? (() => {
|
|
613
643
|
const input = response.usage.prompt_tokens ?? response.usage.input_tokens ?? 0;
|
|
614
644
|
const cached = extractCompatCachedTokens(response.usage);
|
|
@@ -616,10 +646,7 @@ export class OpenAICompatProvider {
|
|
|
616
646
|
// (1 tick = $1e-10, per docs.x.ai). Surface it as costUsd so the
|
|
617
647
|
// session manager skips the catalog-rate fallback and records the
|
|
618
648
|
// provider-billed value verbatim.
|
|
619
|
-
const
|
|
620
|
-
const costUsd = typeof ticks === 'number' && ticks >= 0
|
|
621
|
-
? Number((ticks * 1e-10).toFixed(8))
|
|
622
|
-
: undefined;
|
|
649
|
+
const costUsd = compatReportedCostUsd(this.name, response.usage);
|
|
623
650
|
return {
|
|
624
651
|
inputTokens: input,
|
|
625
652
|
outputTokens: response.usage.completion_tokens ?? response.usage.output_tokens ?? 0,
|
|
@@ -884,6 +911,7 @@ export class OpenAICompatProvider {
|
|
|
884
911
|
// any rejection as a provider-side drop.)
|
|
885
912
|
const nextPreviousResponseId = response.id;
|
|
886
913
|
const encryptedReasoningItems = encryptedXaiReasoningItems(response.output);
|
|
914
|
+
const providerReplay = createProviderReplay('xai-responses', response.output);
|
|
887
915
|
const encryptedReasoningHistory = [
|
|
888
916
|
...(Array.isArray(opts.providerState?.xaiResponses?.encryptedReasoningHistory)
|
|
889
917
|
? opts.providerState.xaiResponses.encryptedReasoningHistory
|
|
@@ -904,6 +932,7 @@ export class OpenAICompatProvider {
|
|
|
904
932
|
...(streamed.stopReason === 'length' && (streamed.content || '').length > 0 ? { truncated: true } : {}),
|
|
905
933
|
citations: searchSources.citations.length ? searchSources.citations : undefined,
|
|
906
934
|
webSearchCalls: searchSources.webSearchCalls.length ? searchSources.webSearchCalls : undefined,
|
|
935
|
+
providerReplay,
|
|
907
936
|
providerState: {
|
|
908
937
|
...(opts.providerState || {}),
|
|
909
938
|
xaiResponses: {
|
|
@@ -1022,6 +1051,7 @@ export class OpenAICompatProvider {
|
|
|
1022
1051
|
// Same rationale as the HTTP path above: `length` stop keeps the chain.
|
|
1023
1052
|
const nextPreviousResponseId = responseId;
|
|
1024
1053
|
const encryptedReasoningItems = encryptedXaiReasoningItems(result.responseItems);
|
|
1054
|
+
const providerReplay = createProviderReplay('xai-responses', result.responseItems);
|
|
1025
1055
|
const encryptedReasoningHistory = [
|
|
1026
1056
|
...(Array.isArray(opts.providerState?.xaiResponses?.encryptedReasoningHistory)
|
|
1027
1057
|
? opts.providerState.xaiResponses.encryptedReasoningHistory
|
|
@@ -1080,6 +1110,7 @@ export class OpenAICompatProvider {
|
|
|
1080
1110
|
// P1 audit fix: same truncated signal as the HTTP path (see
|
|
1081
1111
|
// _doSendXaiResponses above) for the WebSocket transport.
|
|
1082
1112
|
...(result.stopReason === 'length' && (result.content || '').length > 0 ? { truncated: true } : {}),
|
|
1113
|
+
providerReplay,
|
|
1083
1114
|
providerState: {
|
|
1084
1115
|
...(opts.providerState || {}),
|
|
1085
1116
|
xaiResponses: {
|
|
@@ -38,6 +38,7 @@ import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './ant
|
|
|
38
38
|
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
39
39
|
import { CODEX_OAUTH_ORIGINATOR, CODEX_RESPONSES_URL, _displayCodexModel } from './openai-oauth.mjs';
|
|
40
40
|
import { createActiveToolItemTracker } from './tool-stream-state.mjs';
|
|
41
|
+
import { createProviderReplay } from './lib/provider-replay.mjs';
|
|
41
42
|
import { parseProviderJsonBatch } from './stream-json-pool.mjs';
|
|
42
43
|
import { envFlag as _envFlag } from './lib/env-utils.mjs';
|
|
43
44
|
export { envPositiveInt as _envPositiveInt } from './lib/env-utils.mjs';
|
|
@@ -504,6 +505,8 @@ export async function sendViaHttpSse({
|
|
|
504
505
|
const clearActiveToolItem = _toolTracker.clear;
|
|
505
506
|
let _toolInFlight = false;
|
|
506
507
|
const reasoningItems = [];
|
|
508
|
+
const responseItems = [];
|
|
509
|
+
const responseItemKeys = new Set();
|
|
507
510
|
const citations = [];
|
|
508
511
|
const citationKeys = new Set();
|
|
509
512
|
const webSearchCalls = [];
|
|
@@ -663,6 +666,16 @@ export async function sendViaHttpSse({
|
|
|
663
666
|
});
|
|
664
667
|
}
|
|
665
668
|
};
|
|
669
|
+
const pushResponseItem = (item) => {
|
|
670
|
+
if (!item || typeof item !== 'object') return;
|
|
671
|
+
let fallbackKey = '';
|
|
672
|
+
try { fallbackKey = JSON.stringify(item); } catch {}
|
|
673
|
+
const key = `${item.type || 'unknown'}:${item.id || item.call_id || fallbackKey}`;
|
|
674
|
+
if (responseItemKeys.has(key)) return;
|
|
675
|
+
responseItemKeys.add(key);
|
|
676
|
+
try { responseItems.push(structuredClone(item)); }
|
|
677
|
+
catch { responseItems.push({ ...item }); }
|
|
678
|
+
};
|
|
666
679
|
const pushToolSearchCall = (item) => {
|
|
667
680
|
if (!item || item.type !== 'tool_search_call') return;
|
|
668
681
|
const callId = item.call_id || item.id || '';
|
|
@@ -800,6 +813,7 @@ export async function sendViaHttpSse({
|
|
|
800
813
|
break;
|
|
801
814
|
case 'response.output_item.done': {
|
|
802
815
|
const item = event.item || {};
|
|
816
|
+
pushResponseItem(item);
|
|
803
817
|
pushReasoningItem(item);
|
|
804
818
|
pushWebSearchCall(item);
|
|
805
819
|
if (item.type === 'function_call') {
|
|
@@ -852,6 +866,7 @@ export async function sendViaHttpSse({
|
|
|
852
866
|
}
|
|
853
867
|
let reportedBundleProgress = false;
|
|
854
868
|
for (const item of resp.output || []) {
|
|
869
|
+
pushResponseItem(item);
|
|
855
870
|
if (item.type === 'message') {
|
|
856
871
|
for (const part of item.content || []) {
|
|
857
872
|
if (!content && part.type === 'output_text') {
|
|
@@ -1060,6 +1075,11 @@ export async function sendViaHttpSse({
|
|
|
1060
1075
|
// trailing text is never lost (streamed-text path).
|
|
1061
1076
|
flushLeak();
|
|
1062
1077
|
} catch (err) {
|
|
1078
|
+
if (err && !err.partialProviderReplay) {
|
|
1079
|
+
try {
|
|
1080
|
+
err.partialProviderReplay = createProviderReplay('openai-responses', responseItems);
|
|
1081
|
+
} catch {}
|
|
1082
|
+
}
|
|
1063
1083
|
// Live-text invariant: once a non-empty chunk has been relayed it
|
|
1064
1084
|
// cannot be withdrawn — flag the error so no upstream layer retries.
|
|
1065
1085
|
if (emittedText && err) { try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {} }
|
|
@@ -1100,6 +1120,7 @@ export async function sendViaHttpSse({
|
|
|
1100
1120
|
try {
|
|
1101
1121
|
err.partialContent = content;
|
|
1102
1122
|
err.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
1123
|
+
err.partialProviderReplay = createProviderReplay('openai-responses', responseItems);
|
|
1103
1124
|
err.partialModel = model || undefined;
|
|
1104
1125
|
err.pendingToolUse = pendingCalls.size > 0 || activeToolItems.size > 0 || _toolInFlight === true;
|
|
1105
1126
|
} catch { /* best-effort enrichment */ }
|
|
@@ -1141,6 +1162,7 @@ export async function sendViaHttpSse({
|
|
|
1141
1162
|
content,
|
|
1142
1163
|
model: liveModel,
|
|
1143
1164
|
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
1165
|
+
providerReplay: createProviderReplay('openai-responses', responseItems),
|
|
1144
1166
|
toolCalls: _returnedToolCalls,
|
|
1145
1167
|
citations: citations.length ? citations : undefined,
|
|
1146
1168
|
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
@@ -50,7 +50,6 @@ import {
|
|
|
50
50
|
acquireWebSocket,
|
|
51
51
|
releaseWebSocket,
|
|
52
52
|
_sendFrame,
|
|
53
|
-
_closeAllPooledSockets,
|
|
54
53
|
drainOpenaiWsPool,
|
|
55
54
|
} from './openai-ws-pool.mjs';
|
|
56
55
|
import {
|
|
@@ -74,7 +73,6 @@ import { envPositiveInt } from './lib/env-utils.mjs';
|
|
|
74
73
|
// (drainOpenaiWsPool), scripts/provider-toolcall-test.mjs (parseToolSearchArgs,
|
|
75
74
|
// _logicalResponseItemMatch, _streamResponse) and other external callers.
|
|
76
75
|
export {
|
|
77
|
-
_closeAllPooledSockets,
|
|
78
76
|
drainOpenaiWsPool,
|
|
79
77
|
_logicalResponseItemMatch,
|
|
80
78
|
parseToolSearchArgs,
|
|
@@ -17,6 +17,7 @@ import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
|
17
17
|
import { writeJsonAtomicSync, withFileLock } from '../../../shared/atomic-file.mjs';
|
|
18
18
|
import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
|
|
19
19
|
import { makeModelCache } from './model-cache.mjs';
|
|
20
|
+
import { providerReplayItems } from './lib/provider-replay.mjs';
|
|
20
21
|
|
|
21
22
|
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
22
23
|
import { _combineUsageWithWarmup } from './openai-ws-events.mjs';
|
|
@@ -175,6 +176,18 @@ export function convertMessagesToResponsesInput(messages, opts = {}) {
|
|
|
175
176
|
continue;
|
|
176
177
|
}
|
|
177
178
|
flushToolMedia();
|
|
179
|
+
const orderedReplay = replayEncryptedReasoning && m.role === 'assistant'
|
|
180
|
+
? providerReplayItems(m, 'openai-responses')
|
|
181
|
+
: undefined;
|
|
182
|
+
if (orderedReplay?.length) {
|
|
183
|
+
for (const item of orderedReplay) {
|
|
184
|
+
if (item?.type === 'custom_tool_call' && item.call_id) {
|
|
185
|
+
customToolCallNameById.set(item.call_id, item.name || '');
|
|
186
|
+
}
|
|
187
|
+
out.push(item);
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
178
191
|
pushReasoningItems(m, 'before');
|
|
179
192
|
if (m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length) {
|
|
180
193
|
// Default path deliberately omits reasoning replay: openai-oauth
|
|
@@ -184,7 +184,7 @@ function _getPoolArr(poolKey) {
|
|
|
184
184
|
// cap-overflow ephemeral. They were invisible to per-session close and to the
|
|
185
185
|
// global drain, so a wedged one survived both. Tracking them here keeps the
|
|
186
186
|
// reuse map unchanged (nothing can select them) while making every owned socket
|
|
187
|
-
// reachable by closeOpenaiWsPoolForSession/
|
|
187
|
+
// reachable by closeOpenaiWsPoolForSession/drainOpenaiWsPool.
|
|
188
188
|
const _unpooledSockets = new Set();
|
|
189
189
|
|
|
190
190
|
function _trackUnpooled(entry, poolKey) {
|
|
@@ -883,16 +883,14 @@ export function _setOpenSocketForTest(fn) {
|
|
|
883
883
|
_openSocketImpl = typeof fn === 'function' ? fn : _openSocket;
|
|
884
884
|
}
|
|
885
885
|
|
|
886
|
-
// Drain-complete fence — set true once
|
|
886
|
+
// Drain-complete fence — set true once drainOpenaiWsPool runs so any
|
|
887
887
|
// in-flight acquire that resumes after drain throws instead of pushing a
|
|
888
888
|
// fresh socket into the cleared pool. Single-set, process-lifetime invariant.
|
|
889
889
|
let _drainComplete = false;
|
|
890
890
|
|
|
891
891
|
// Drain hook — self-registered exit drain.
|
|
892
892
|
// Force-closes pooled sockets and fences subsequent acquires.
|
|
893
|
-
|
|
894
|
-
// `_closeAllPooledSockets` kept for backward compat with existing call sites.
|
|
895
|
-
export function _closeAllPooledSockets(reason = 'shutdown', { immediate = false } = {}) {
|
|
893
|
+
export function drainOpenaiWsPool(reason = 'shutdown', { immediate = false } = {}) {
|
|
896
894
|
_drainComplete = true;
|
|
897
895
|
for (const arr of _wsPool.values()) {
|
|
898
896
|
// _shutdownEntry tears down per-entry timers before the map is dropped
|
|
@@ -909,7 +907,6 @@ export function _closeAllPooledSockets(reason = 'shutdown', { immediate = false
|
|
|
909
907
|
_unpooledSockets.clear();
|
|
910
908
|
_wsPool.clear();
|
|
911
909
|
}
|
|
912
|
-
export const drainOpenaiWsPool = _closeAllPooledSockets;
|
|
913
910
|
// Session-close / drain hooks are shared across provider transports (cursor-wire
|
|
914
911
|
// registers the same globals), so chain whatever is already installed instead of
|
|
915
912
|
// replacing it — import order must not decide which transport gets torn down.
|
|
@@ -925,4 +922,4 @@ globalThis.__mixdogDrainProviderConnections = (reason) => {
|
|
|
925
922
|
};
|
|
926
923
|
// 'exit' hands the listener an exit CODE, never a reason string, and no timer
|
|
927
924
|
// can fire after it: terminate immediately instead of scheduling.
|
|
928
|
-
process.on('exit', () =>
|
|
925
|
+
process.on('exit', () => drainOpenaiWsPool('process-exit', { immediate: true }));
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
streamStalledError,
|
|
34
34
|
} from '../stall-policy.mjs';
|
|
35
35
|
import { customToolCallFromResponseItem } from './custom-tool-wire.mjs';
|
|
36
|
+
import { createProviderReplay } from './lib/provider-replay.mjs';
|
|
36
37
|
import { _wsErrLabel, WS_MAX_INCOMING_FRAME_BYTES } from './openai-ws-pool.mjs';
|
|
37
38
|
import {
|
|
38
39
|
_sansInput,
|
|
@@ -675,6 +676,10 @@ export async function _streamResponse({
|
|
|
675
676
|
try {
|
|
676
677
|
err.partialContent = content;
|
|
677
678
|
err.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
679
|
+
err.partialProviderReplay = createProviderReplay(
|
|
680
|
+
traceProvider === 'xai' ? 'xai-responses' : 'openai-responses',
|
|
681
|
+
responseItemsAdded,
|
|
682
|
+
);
|
|
678
683
|
err.pendingToolUse = pendingCalls.size > 0
|
|
679
684
|
|| activeToolItems.size > 0
|
|
680
685
|
|| _toolInFlight === true;
|
|
@@ -794,6 +799,10 @@ export async function _streamResponse({
|
|
|
794
799
|
model,
|
|
795
800
|
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
796
801
|
responseItems: responseItemsAdded.length ? responseItemsAdded : undefined,
|
|
802
|
+
providerReplay: createProviderReplay(
|
|
803
|
+
traceProvider === 'xai' ? 'xai-responses' : 'openai-responses',
|
|
804
|
+
responseItemsAdded,
|
|
805
|
+
),
|
|
797
806
|
// Dedupe by name+args (Fix 2, array side) so an identical
|
|
798
807
|
// synthetic-leaked + native pair can't run the tool twice.
|
|
799
808
|
toolCalls: toolCalls.length ? dedupeToolCallList(toolCalls) : undefined,
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createProviderReplay,
|
|
6
|
+
providerReplayItems,
|
|
7
|
+
} from './lib/provider-replay.mjs';
|
|
8
|
+
import { toAnthropicMessages } from './lib/anthropic-request-utils.mjs';
|
|
9
|
+
import { parseSSEStream as parseAnthropicSSEStream } from './anthropic-sse.mjs';
|
|
10
|
+
import { toGeminiContents } from './gemini-schema.mjs';
|
|
11
|
+
import { convertMessagesToResponsesInput } from './openai-responses-payload.mjs';
|
|
12
|
+
import { toOpenAIMessages, toXaiResponsesInput } from './openai-compat-wire.mjs';
|
|
13
|
+
import { estimateMessagesTokens } from '../session/context-utils.mjs';
|
|
14
|
+
import { _sessionForDisk } from '../session/store/serialize.mjs';
|
|
15
|
+
|
|
16
|
+
function interleavedAnthropicBlocks() {
|
|
17
|
+
const blocks = [{ type: 'thinking', thinking: '', signature: 'sig-a' }];
|
|
18
|
+
for (let index = 1; index <= 10; index += 1) {
|
|
19
|
+
blocks.push({
|
|
20
|
+
type: 'tool_use',
|
|
21
|
+
id: `toolu_${index}`,
|
|
22
|
+
name: 'read',
|
|
23
|
+
input: { index },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
blocks.push({ type: 'thinking', thinking: '', signature: 'sig-b' });
|
|
27
|
+
blocks.push({
|
|
28
|
+
type: 'tool_use',
|
|
29
|
+
id: 'toolu_11',
|
|
30
|
+
name: 'read',
|
|
31
|
+
input: { index: 11 },
|
|
32
|
+
});
|
|
33
|
+
return blocks;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function interleavedResponsesItems() {
|
|
37
|
+
const items = [{
|
|
38
|
+
type: 'reasoning',
|
|
39
|
+
id: 'rs_1',
|
|
40
|
+
encrypted_content: 'cipher-a',
|
|
41
|
+
summary: [],
|
|
42
|
+
}];
|
|
43
|
+
for (let index = 1; index <= 10; index += 1) {
|
|
44
|
+
items.push({
|
|
45
|
+
type: 'function_call',
|
|
46
|
+
call_id: `call_${index}`,
|
|
47
|
+
name: 'read',
|
|
48
|
+
arguments: JSON.stringify({ index }),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
items.push({
|
|
52
|
+
type: 'reasoning',
|
|
53
|
+
id: 'rs_2',
|
|
54
|
+
encrypted_content: 'cipher-b',
|
|
55
|
+
summary: [],
|
|
56
|
+
});
|
|
57
|
+
items.push({
|
|
58
|
+
type: 'function_call',
|
|
59
|
+
call_id: 'call_11',
|
|
60
|
+
name: 'read',
|
|
61
|
+
arguments: '{"index":11}',
|
|
62
|
+
});
|
|
63
|
+
return items;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function anthropicSseResponse(events) {
|
|
67
|
+
const body = events
|
|
68
|
+
.map((event) => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`)
|
|
69
|
+
.join('');
|
|
70
|
+
return new Response(body, { headers: { 'content-type': 'text/event-stream' } });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test('provider replay is detached and scoped to its originating provider', () => {
|
|
74
|
+
const source = [{ type: 'thinking', thinking: 'plan', signature: 'sig' }];
|
|
75
|
+
const replay = createProviderReplay('anthropic', source);
|
|
76
|
+
source[0].signature = 'mutated';
|
|
77
|
+
|
|
78
|
+
assert.equal(replay.items[0].signature, 'sig');
|
|
79
|
+
assert.deepEqual(providerReplayItems({ providerReplay: replay }, 'anthropic'), replay.items);
|
|
80
|
+
assert.equal(providerReplayItems({ providerReplay: replay }, 'gemini'), undefined);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('Anthropic keeps interleaved thinking at content[11]', () => {
|
|
84
|
+
const blocks = interleavedAnthropicBlocks();
|
|
85
|
+
const message = {
|
|
86
|
+
role: 'assistant',
|
|
87
|
+
content: '',
|
|
88
|
+
toolCalls: blocks
|
|
89
|
+
.filter((block) => block.type === 'tool_use')
|
|
90
|
+
.map((block) => ({
|
|
91
|
+
id: block.id,
|
|
92
|
+
name: block.name,
|
|
93
|
+
arguments: block.input,
|
|
94
|
+
})),
|
|
95
|
+
providerReplay: createProviderReplay('anthropic', blocks),
|
|
96
|
+
};
|
|
97
|
+
const lowered = toAnthropicMessages([message]);
|
|
98
|
+
|
|
99
|
+
assert.deepEqual(lowered[0].content, blocks);
|
|
100
|
+
assert.equal(lowered[0].content[11].type, 'thinking');
|
|
101
|
+
assert.equal(lowered[0].content[11].signature, 'sig-b');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('Anthropic SSE parse to next-request replay keeps content[11] unchanged', async () => {
|
|
105
|
+
const events = [
|
|
106
|
+
{ type: 'message_start', message: { model: 'claude-test', usage: { input_tokens: 1 } } },
|
|
107
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: '' } },
|
|
108
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'signature_delta', signature: 'sig-a' } },
|
|
109
|
+
{ type: 'content_block_stop', index: 0 },
|
|
110
|
+
];
|
|
111
|
+
for (let index = 1; index <= 10; index += 1) {
|
|
112
|
+
events.push(
|
|
113
|
+
{
|
|
114
|
+
type: 'content_block_start',
|
|
115
|
+
index,
|
|
116
|
+
content_block: { type: 'tool_use', id: `toolu_${index}`, name: 'read' },
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'content_block_delta',
|
|
120
|
+
index,
|
|
121
|
+
delta: { type: 'input_json_delta', partial_json: JSON.stringify({ index }) },
|
|
122
|
+
},
|
|
123
|
+
{ type: 'content_block_stop', index },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
events.push(
|
|
127
|
+
{ type: 'content_block_start', index: 11, content_block: { type: 'thinking', thinking: '' } },
|
|
128
|
+
{ type: 'content_block_delta', index: 11, delta: { type: 'signature_delta', signature: 'sig-b' } },
|
|
129
|
+
{ type: 'content_block_stop', index: 11 },
|
|
130
|
+
{
|
|
131
|
+
type: 'content_block_start',
|
|
132
|
+
index: 12,
|
|
133
|
+
content_block: { type: 'tool_use', id: 'toolu_11', name: 'read' },
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: 'content_block_delta',
|
|
137
|
+
index: 12,
|
|
138
|
+
delta: { type: 'input_json_delta', partial_json: '{"index":11}' },
|
|
139
|
+
},
|
|
140
|
+
{ type: 'content_block_stop', index: 12 },
|
|
141
|
+
{ type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
|
|
142
|
+
{ type: 'message_stop' },
|
|
143
|
+
);
|
|
144
|
+
const parsed = await parseAnthropicSSEStream(
|
|
145
|
+
anthropicSseResponse(events),
|
|
146
|
+
null,
|
|
147
|
+
() => {},
|
|
148
|
+
() => {},
|
|
149
|
+
() => {},
|
|
150
|
+
{},
|
|
151
|
+
null,
|
|
152
|
+
new Set(['read']),
|
|
153
|
+
);
|
|
154
|
+
const lowered = toAnthropicMessages([{
|
|
155
|
+
role: 'assistant',
|
|
156
|
+
content: parsed.content || '',
|
|
157
|
+
toolCalls: parsed.toolCalls,
|
|
158
|
+
providerReplay: parsed.providerReplay,
|
|
159
|
+
}]);
|
|
160
|
+
|
|
161
|
+
assert.equal(parsed.providerReplay.items[11].type, 'thinking');
|
|
162
|
+
assert.deepEqual(lowered[0].content, parsed.providerReplay.items);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('Gemini keeps signed thought Parts interleaved with function calls', () => {
|
|
166
|
+
const parts = [{ text: 'plan-a', thought: true, thoughtSignature: 'sig-a' }];
|
|
167
|
+
for (let index = 1; index <= 10; index += 1) {
|
|
168
|
+
parts.push({ functionCall: { id: `call_${index}`, name: 'read', args: { index } } });
|
|
169
|
+
}
|
|
170
|
+
parts.push({ text: 'plan-b', thought: true, thoughtSignature: 'sig-b' });
|
|
171
|
+
parts.push({ functionCall: { id: 'call_11', name: 'read', args: { index: 11 } } });
|
|
172
|
+
const contents = toGeminiContents([{
|
|
173
|
+
role: 'assistant',
|
|
174
|
+
content: '',
|
|
175
|
+
toolCalls: [],
|
|
176
|
+
providerReplay: createProviderReplay('gemini', parts),
|
|
177
|
+
}], 'gemini-3-pro');
|
|
178
|
+
|
|
179
|
+
assert.deepEqual(contents[0].parts, parts);
|
|
180
|
+
assert.equal(contents[0].parts[11].thoughtSignature, 'sig-b');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('OpenAI Responses recovery keeps reasoning between function calls', () => {
|
|
184
|
+
const items = interleavedResponsesItems();
|
|
185
|
+
const input = convertMessagesToResponsesInput([{
|
|
186
|
+
role: 'assistant',
|
|
187
|
+
content: '',
|
|
188
|
+
toolCalls: [],
|
|
189
|
+
providerReplay: createProviderReplay('openai-responses', items),
|
|
190
|
+
}], { replayEncryptedReasoning: true });
|
|
191
|
+
|
|
192
|
+
assert.deepEqual(input, items);
|
|
193
|
+
assert.equal(input[11].type, 'reasoning');
|
|
194
|
+
assert.equal(input[11].id, 'rs_2');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('xAI stateless recovery keeps encrypted reasoning output order', () => {
|
|
198
|
+
const items = interleavedResponsesItems();
|
|
199
|
+
const result = toXaiResponsesInput([{
|
|
200
|
+
role: 'assistant',
|
|
201
|
+
content: '',
|
|
202
|
+
toolCalls: [],
|
|
203
|
+
providerReplay: createProviderReplay('xai-responses', items),
|
|
204
|
+
}], null);
|
|
205
|
+
|
|
206
|
+
assert.deepEqual(result.input, items);
|
|
207
|
+
assert.equal(result.input[11].type, 'reasoning');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('OpenRouter reasoning_details remains an unmodified assistant field', () => {
|
|
211
|
+
const reasoningDetails = [
|
|
212
|
+
{ type: 'reasoning.text', text: 'plan', signature: 'opaque' },
|
|
213
|
+
{ type: 'reasoning.encrypted', data: 'ciphertext' },
|
|
214
|
+
];
|
|
215
|
+
const wire = toOpenAIMessages([{
|
|
216
|
+
role: 'assistant',
|
|
217
|
+
content: '',
|
|
218
|
+
toolCalls: [{ id: 'call_1', name: 'read', arguments: { path: 'a' } }],
|
|
219
|
+
providerMetadata: { openrouter: { reasoning_details: reasoningDetails } },
|
|
220
|
+
}], 'openrouter');
|
|
221
|
+
|
|
222
|
+
assert.equal(wire[0].reasoning_details, reasoningDetails);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('provider replay survives disk projection and contributes to context size', () => {
|
|
226
|
+
const providerReplay = createProviderReplay('openai-responses', interleavedResponsesItems());
|
|
227
|
+
const message = { role: 'assistant', content: '', providerReplay };
|
|
228
|
+
const stored = _sessionForDisk({ id: 's', messages: [message] });
|
|
229
|
+
|
|
230
|
+
assert.deepEqual(stored.messages[0].providerReplay, providerReplay);
|
|
231
|
+
assert.ok(
|
|
232
|
+
estimateMessagesTokens([message])
|
|
233
|
+
> estimateMessagesTokens([{ role: 'assistant', content: '' }]),
|
|
234
|
+
);
|
|
235
|
+
});
|