mixdog 0.9.0 → 0.9.2
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 +10 -3
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +12 -4
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +146 -9
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +0 -11
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -647
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +81 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +26 -27
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +154 -20
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -29,13 +29,17 @@ import {
|
|
|
29
29
|
traceAgentUsage,
|
|
30
30
|
} from '../agent-trace.mjs';
|
|
31
31
|
import {
|
|
32
|
-
PROVIDER_GENERATE_TOTAL_TIMEOUT_MS,
|
|
33
32
|
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
33
|
+
PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
|
|
34
|
+
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
35
|
+
streamStalledError,
|
|
34
36
|
createTimeoutSignal,
|
|
37
|
+
createPassthroughSignal,
|
|
35
38
|
} from '../stall-policy.mjs';
|
|
36
|
-
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
39
|
+
import { populateHttpStatusFromMessage, shouldFallbackTransport } from './retry-classifier.mjs';
|
|
37
40
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
38
41
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
42
|
+
import { createLeakGuard, createToolCallDedupe, dedupeToolCallList } from './anthropic-leaked-toolcall.mjs';
|
|
39
43
|
import {
|
|
40
44
|
normalizeContentForOpenAIResponses,
|
|
41
45
|
splitToolContentForOpenAIResponses,
|
|
@@ -160,6 +164,17 @@ function _displayCodexModel(id) {
|
|
|
160
164
|
return id.replace(/-\d{4}-\d{2}-\d{2}$/, '');
|
|
161
165
|
}
|
|
162
166
|
|
|
167
|
+
function _positiveCodexContextWindow(value) {
|
|
168
|
+
const n = Number(value);
|
|
169
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _codexContextWindowFromApi(m) {
|
|
173
|
+
return _positiveCodexContextWindow(m?.context_window)
|
|
174
|
+
|| _positiveCodexContextWindow(m?.max_context_window)
|
|
175
|
+
|| null;
|
|
176
|
+
}
|
|
177
|
+
|
|
163
178
|
function _normalizeCodexModel(m) {
|
|
164
179
|
const id = m?.slug || m?.id;
|
|
165
180
|
const family = _codexFamily(id);
|
|
@@ -182,8 +197,8 @@ function _normalizeCodexModel(m) {
|
|
|
182
197
|
display: m?.display_name || id,
|
|
183
198
|
family,
|
|
184
199
|
provider: 'openai-oauth',
|
|
185
|
-
contextWindow: m
|
|
186
|
-
maxContextWindow: m?.max_context_window
|
|
200
|
+
contextWindow: _codexContextWindowFromApi(m),
|
|
201
|
+
maxContextWindow: _positiveCodexContextWindow(m?.max_context_window),
|
|
187
202
|
outputTokens: m?.max_output_tokens || m?.output_tokens || 32768,
|
|
188
203
|
autoCompactTokenLimit: m?.auto_compact_token_limit || null,
|
|
189
204
|
effectiveContextWindowPercent: m?.effective_context_window_percent || null,
|
|
@@ -742,36 +757,14 @@ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
|
|
|
742
757
|
return headers;
|
|
743
758
|
}
|
|
744
759
|
|
|
760
|
+
// WS→HTTP/SSE fallback predicate → shared shouldFallbackTransport
|
|
761
|
+
// (retry-classifier.mjs). The per-provider env flag is computed here and passed
|
|
762
|
+
// as `enabled`; the deny-order + allow-list are identical to the former copy.
|
|
745
763
|
function _shouldUseOpenAIHttpFallback(err, externalSignal) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
// concatenate a second attempt onto rendered output. Never fall back.
|
|
751
|
-
if (err?.liveTextEmitted === true) return false;
|
|
752
|
-
if (err?.emittedToolCall === true || err?.unsafeToRetry === true) return false;
|
|
753
|
-
const status = Number(err?.httpStatus || err?.status || 0);
|
|
754
|
-
if (status === 401 || status === 403 || status === 404 || status === 429) return false;
|
|
755
|
-
if (status >= 500 && status < 600) return true;
|
|
756
|
-
const code = String(err?.code || '');
|
|
757
|
-
if (['EWSACQUIRETIMEOUT', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ENOTFOUND', 'EAI_NODATA', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'EPIPE'].includes(code)) {
|
|
758
|
-
return true;
|
|
759
|
-
}
|
|
760
|
-
const classifier = String(err?.retryClassifier || err?.midstreamClassifier || '');
|
|
761
|
-
if ([
|
|
762
|
-
'timeout', 'reset', 'dns', 'refused', 'network', 'acquire_timeout', 'http_5xx',
|
|
763
|
-
'first_byte_timeout', 'first_meaningful_timeout',
|
|
764
|
-
'ws_1006', 'ws_1011', 'ws_1012', 'ws_1000', 'ws_4000', 'agent_stall', 'stream_stalled',
|
|
765
|
-
'response_failed_disconnected', 'response_failed_network', 'response_failed_auth_expired',
|
|
766
|
-
'ws_send_failed',
|
|
767
|
-
].includes(classifier)) {
|
|
768
|
-
return true;
|
|
769
|
-
}
|
|
770
|
-
if (/^http_5\d\d$/.test(classifier)) return true;
|
|
771
|
-
if (err?.firstByteTimeout) return true;
|
|
772
|
-
if (err?.firstMeaningfulTimeout) return true;
|
|
773
|
-
const msg = String(err?.message || '');
|
|
774
|
-
return /opening handshake has timed out|socket hang up|acquire timed out|no first server event|no meaningful output/i.test(msg);
|
|
764
|
+
return shouldFallbackTransport(err, {
|
|
765
|
+
signal: externalSignal,
|
|
766
|
+
enabled: _envFlag('MIXDOG_OPENAI_OAUTH_HTTP_FALLBACK', true),
|
|
767
|
+
});
|
|
775
768
|
}
|
|
776
769
|
|
|
777
770
|
// Exported for the single-emit regression smoke (scripts/openai-oauth-
|
|
@@ -795,11 +788,20 @@ export async function sendViaHttpSse({
|
|
|
795
788
|
useModel,
|
|
796
789
|
fetchFn = fetch,
|
|
797
790
|
} = {}) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
)
|
|
791
|
+
// P1 audit fix: no fixed wall-clock total cap on the HTTP/SSE fallback
|
|
792
|
+
// stream. The old createTimeoutSignal(..., PROVIDER_GENERATE_TOTAL_TIMEOUT_MS)
|
|
793
|
+
// killed a healthy, still-streaming turn purely on elapsed time, unlike
|
|
794
|
+
// every other streaming provider path (anthropic-oauth uses the same
|
|
795
|
+
// createPassthroughSignal pattern — see anthropic-oauth.mjs "Option A").
|
|
796
|
+
// The stream is bounded instead by:
|
|
797
|
+
// (a) headerTimeout below (PROVIDER_HTTP_RESPONSE_TIMEOUT_MS) for a
|
|
798
|
+
// socket that never sends the initial response,
|
|
799
|
+
// (b) the SEMANTIC idle watchdog (_armSemanticIdle /
|
|
800
|
+
// PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS), which resets on every
|
|
801
|
+
// meaningful() chunk — a live stream stays alive, a truly silent
|
|
802
|
+
// one still aborts, and
|
|
803
|
+
// (c) externalSignal (client disconnect / replaced-by-newer-request).
|
|
804
|
+
const totalTimeout = createPassthroughSignal(externalSignal);
|
|
803
805
|
const headerTimeout = createTimeoutSignal(
|
|
804
806
|
totalTimeout.signal,
|
|
805
807
|
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
@@ -873,6 +875,33 @@ export async function sendViaHttpSse({
|
|
|
873
875
|
if (totalTimeout.signal.aborted) _onTotalAbort();
|
|
874
876
|
else totalTimeout.signal.addEventListener('abort', _onTotalAbort, { once: true });
|
|
875
877
|
}
|
|
878
|
+
// SEMANTIC idle watchdog: reset ONLY on meaningful() (text/reasoning/tool
|
|
879
|
+
// deltas), never on raw bytes/keepalive frames, so a stream that emits some
|
|
880
|
+
// deltas then goes silent trips a short, named terminal failure instead of
|
|
881
|
+
// hanging until the 30-min agent watchdog. Disablable via the shared env.
|
|
882
|
+
let _semanticIdleTimer = null;
|
|
883
|
+
const _clearSemanticIdle = () => {
|
|
884
|
+
if (_semanticIdleTimer) { clearTimeout(_semanticIdleTimer); _semanticIdleTimer = null; }
|
|
885
|
+
};
|
|
886
|
+
const _armSemanticIdle = () => {
|
|
887
|
+
if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED || !(PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS > 0)) return;
|
|
888
|
+
_clearSemanticIdle();
|
|
889
|
+
_semanticIdleTimer = setTimeout(() => {
|
|
890
|
+
_streamAbortReason = streamStalledError('OpenAI OAuth HTTP fallback', PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS, { emittedToolCall: emittedToolCallIds.size > 0 });
|
|
891
|
+
// Partial-final recovery parity with anthropic-oauth: attach the
|
|
892
|
+
// streamed partial state so the agent loop can accept a wedged FINAL
|
|
893
|
+
// no-tool summary as a successful partial-final instead of dropping
|
|
894
|
+
// the result. pendingToolUse gates out any mid-flight tool call.
|
|
895
|
+
try {
|
|
896
|
+
_streamAbortReason.partialContent = content;
|
|
897
|
+
_streamAbortReason.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
898
|
+
_streamAbortReason.pendingToolUse = pendingCalls.size > 0 || emittedToolCallIds.size > 0;
|
|
899
|
+
_streamAbortReason.partialModel = model || undefined;
|
|
900
|
+
} catch { /* best-effort enrichment */ }
|
|
901
|
+
try { reader.cancel(_streamAbortReason).catch(() => {}); } catch {}
|
|
902
|
+
}, PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS);
|
|
903
|
+
try { _semanticIdleTimer.unref?.(); } catch {}
|
|
904
|
+
};
|
|
876
905
|
let buffer = '';
|
|
877
906
|
let content = '';
|
|
878
907
|
let model = '';
|
|
@@ -907,13 +936,75 @@ export async function sendViaHttpSse({
|
|
|
907
936
|
// first complete frame still emits; only redundant re-emits are
|
|
908
937
|
// suppressed.
|
|
909
938
|
const emittedToolCallIds = new Set();
|
|
939
|
+
// Fix 2: cross-path name+args dedupe. A text-leaked synthetic and an
|
|
940
|
+
// identical native function_call must fire onToolCall exactly once.
|
|
941
|
+
const _toolDedupe = createToolCallDedupe();
|
|
910
942
|
const emitToolCall = (call) => {
|
|
911
943
|
if (!call || !call.id) return;
|
|
912
944
|
if (emittedToolCallIds.has(call.id)) return;
|
|
913
945
|
emittedToolCallIds.add(call.id);
|
|
946
|
+
if (!_toolDedupe.shouldDispatch(call.name, call.arguments)) return;
|
|
914
947
|
try { onToolCall?.(call); } catch {}
|
|
915
948
|
};
|
|
916
949
|
|
|
950
|
+
// Leaked tool-call guard. The model sometimes emits a tool call as plain
|
|
951
|
+
// text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
|
|
952
|
+
// `<|channel|>...to=functions.NAME...<|call|>`) inside
|
|
953
|
+
// `response.output_text.delta` instead of a native function_call. Route
|
|
954
|
+
// text through the guard so leaked calls are suppressed from the visible
|
|
955
|
+
// stream, synthesized (native `call_...` id shape), and dispatched like
|
|
956
|
+
// native ones. Known tool names come from the request body so recovery
|
|
957
|
+
// only fires for tools the model was actually offered. Additive: the
|
|
958
|
+
// native function_call path is untouched.
|
|
959
|
+
const _leakKnownTools = new Set(
|
|
960
|
+
(Array.isArray(body?.tools) ? body.tools : [])
|
|
961
|
+
.map((t) => (typeof t?.name === 'string' ? t.name : null))
|
|
962
|
+
.filter(Boolean),
|
|
963
|
+
);
|
|
964
|
+
const leakGuard = createLeakGuard({ knownToolNames: _leakKnownTools, harmony: true });
|
|
965
|
+
const dispatchLeakedCall = (recovered) => {
|
|
966
|
+
let args = recovered?.arguments;
|
|
967
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
968
|
+
const call = {
|
|
969
|
+
id: `call_leaked_${randomBytes(8).toString('hex')}`,
|
|
970
|
+
name: recovered.name,
|
|
971
|
+
arguments: args,
|
|
972
|
+
};
|
|
973
|
+
toolCalls.push(call);
|
|
974
|
+
emitToolCall(call);
|
|
975
|
+
};
|
|
976
|
+
const relayLeakText = (delta) => {
|
|
977
|
+
if (!leakGuard.enabled) {
|
|
978
|
+
content += delta || '';
|
|
979
|
+
if (delta && onTextDelta) {
|
|
980
|
+
emittedText = true;
|
|
981
|
+
try { onTextDelta(delta); } catch {}
|
|
982
|
+
}
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const { text, calls } = leakGuard.push(delta);
|
|
986
|
+
if (text) {
|
|
987
|
+
content += text;
|
|
988
|
+
if (onTextDelta) {
|
|
989
|
+
emittedText = true;
|
|
990
|
+
try { onTextDelta(text); } catch {}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
994
|
+
};
|
|
995
|
+
const flushLeak = () => {
|
|
996
|
+
if (!leakGuard.enabled) return;
|
|
997
|
+
const { text, calls } = leakGuard.flush();
|
|
998
|
+
if (text) {
|
|
999
|
+
content += text;
|
|
1000
|
+
if (onTextDelta) {
|
|
1001
|
+
emittedText = true;
|
|
1002
|
+
try { onTextDelta(text); } catch {}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
1006
|
+
};
|
|
1007
|
+
|
|
917
1008
|
const pushWebSearchCall = (item) => {
|
|
918
1009
|
if (!item || item.type !== 'web_search_call') return;
|
|
919
1010
|
const key = item.id || JSON.stringify(item.action || item);
|
|
@@ -961,6 +1052,7 @@ export async function sendViaHttpSse({
|
|
|
961
1052
|
};
|
|
962
1053
|
const meaningful = () => {
|
|
963
1054
|
if (ttftMs == null) ttftMs = Date.now() - sseStartedAt;
|
|
1055
|
+
_armSemanticIdle();
|
|
964
1056
|
try { onStreamDelta?.(); } catch {}
|
|
965
1057
|
};
|
|
966
1058
|
const handleEvent = (event) => {
|
|
@@ -971,12 +1063,8 @@ export async function sendViaHttpSse({
|
|
|
971
1063
|
if (event.response?.id) responseId = event.response.id;
|
|
972
1064
|
break;
|
|
973
1065
|
case 'response.output_text.delta':
|
|
974
|
-
content += event.delta || '';
|
|
975
1066
|
meaningful();
|
|
976
|
-
|
|
977
|
-
emittedText = true;
|
|
978
|
-
try { onTextDelta(event.delta); } catch {}
|
|
979
|
-
}
|
|
1067
|
+
relayLeakText(event.delta || '');
|
|
980
1068
|
break;
|
|
981
1069
|
case 'response.reasoning_text.delta':
|
|
982
1070
|
case 'response.reasoning_summary_text.delta':
|
|
@@ -1052,7 +1140,20 @@ export async function sendViaHttpSse({
|
|
|
1052
1140
|
for (const item of resp.output || []) {
|
|
1053
1141
|
if (item.type === 'message') {
|
|
1054
1142
|
for (const part of item.content || []) {
|
|
1055
|
-
if (!content && part.type === 'output_text')
|
|
1143
|
+
if (!content && part.type === 'output_text') {
|
|
1144
|
+
// Completed-output fallback (no streamed text).
|
|
1145
|
+
// Route through the leak guard so a tool call
|
|
1146
|
+
// leaked only in the final bundle is recovered
|
|
1147
|
+
// rather than surfaced as visible content. push
|
|
1148
|
+
// with final=true flushes fully (no held tail).
|
|
1149
|
+
if (leakGuard.enabled) {
|
|
1150
|
+
const { text, calls } = leakGuard.push(part.text || '', true);
|
|
1151
|
+
content += text;
|
|
1152
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
1153
|
+
} else {
|
|
1154
|
+
content += part.text || '';
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1056
1157
|
if (part.type === 'output_text') _pushOutputTextAnnotations(part, citations, citationKeys);
|
|
1057
1158
|
}
|
|
1058
1159
|
} else if (item.type === 'reasoning') {
|
|
@@ -1138,10 +1239,12 @@ export async function sendViaHttpSse({
|
|
|
1138
1239
|
|
|
1139
1240
|
try {
|
|
1140
1241
|
while (true) {
|
|
1141
|
-
if (totalTimeout.signal
|
|
1242
|
+
if (totalTimeout.signal?.aborted) {
|
|
1243
|
+
_clearSemanticIdle();
|
|
1142
1244
|
const reason = totalTimeout.signal.reason;
|
|
1143
1245
|
throw reason instanceof Error ? reason : new Error('OpenAI OAuth HTTP fallback aborted');
|
|
1144
1246
|
}
|
|
1247
|
+
if (_streamAbortReason) throw _streamAbortReason;
|
|
1145
1248
|
const { value, done } = await reader.read();
|
|
1146
1249
|
if (done) break;
|
|
1147
1250
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -1162,12 +1265,16 @@ export async function sendViaHttpSse({
|
|
|
1162
1265
|
const event = _parseSseFrame(frame);
|
|
1163
1266
|
if (event) handleEvent(event);
|
|
1164
1267
|
}
|
|
1268
|
+
// Flush any partial-sentinel tail held back mid-stream so legitimate
|
|
1269
|
+
// trailing text is never lost (streamed-text path).
|
|
1270
|
+
flushLeak();
|
|
1165
1271
|
} catch (err) {
|
|
1166
1272
|
// Live-text invariant: once a non-empty chunk has been relayed it
|
|
1167
1273
|
// cannot be withdrawn — flag the error so no upstream layer retries.
|
|
1168
1274
|
if (emittedText && err) { try { err.liveTextEmitted = true; err.unsafeToRetry = true; } catch {} }
|
|
1169
1275
|
throw err;
|
|
1170
1276
|
} finally {
|
|
1277
|
+
_clearSemanticIdle();
|
|
1171
1278
|
try { reader.releaseLock?.(); } catch {}
|
|
1172
1279
|
if (_onTotalAbort && totalTimeout.signal) {
|
|
1173
1280
|
try { totalTimeout.signal.removeEventListener('abort', _onTotalAbort); } catch {}
|
|
@@ -1208,15 +1315,27 @@ export async function sendViaHttpSse({
|
|
|
1208
1315
|
serviceTier,
|
|
1209
1316
|
});
|
|
1210
1317
|
}
|
|
1318
|
+
// Dedupe the returned array by name+args (Fix 2, array side): a synthetic
|
|
1319
|
+
// leaked call and an identical native function_call must not both survive,
|
|
1320
|
+
// else the agent loop executes the side-effecting tool twice.
|
|
1321
|
+
const _returnedToolCalls = toolCalls.length
|
|
1322
|
+
? dedupeToolCallList(toolCalls.map(({ _pendingItemId, ...t }) => t))
|
|
1323
|
+
: undefined;
|
|
1211
1324
|
return {
|
|
1212
1325
|
content,
|
|
1213
1326
|
model: liveModel,
|
|
1214
1327
|
reasoningItems: reasoningItems.length ? reasoningItems : undefined,
|
|
1215
|
-
toolCalls:
|
|
1328
|
+
toolCalls: _returnedToolCalls,
|
|
1216
1329
|
citations: citations.length ? citations : undefined,
|
|
1217
1330
|
webSearchCalls: webSearchCalls.length ? webSearchCalls : undefined,
|
|
1218
1331
|
usage: usage || undefined,
|
|
1219
1332
|
stopReason: stopReason || undefined,
|
|
1333
|
+
// P1 audit fix: text-only max-output cutoff (openai-oauth HTTP/SSE
|
|
1334
|
+
// fallback maps status:'incomplete'/reason=max_output_tokens to
|
|
1335
|
+
// stopReason='length' above and treats it as success). Flag it so
|
|
1336
|
+
// loop.mjs can surface a truncation warning instead of accepting
|
|
1337
|
+
// silently-cut content as a clean final answer.
|
|
1338
|
+
...(stopReason === 'length' && content.length > 0 ? { truncated: true } : {}),
|
|
1220
1339
|
responseId: responseId || undefined,
|
|
1221
1340
|
serviceTier: serviceTier || undefined,
|
|
1222
1341
|
};
|
|
@@ -1350,8 +1469,8 @@ export class OpenAIOAuthProvider {
|
|
|
1350
1469
|
const onTextDelta = typeof opts.onTextDelta === 'function' ? opts.onTextDelta : null;
|
|
1351
1470
|
const externalSignal = opts.signal || null;
|
|
1352
1471
|
const _sendSessionId = opts.sessionId || '(none)';
|
|
1353
|
-
const
|
|
1354
|
-
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-start sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)}
|
|
1472
|
+
const _sendAgent = opts.agent || '(none)';
|
|
1473
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] auth-start sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} agent=${_sendAgent} expiringInMs=${this.tokens?.expires_at ? this.tokens.expires_at - Date.now() : 'unknown'}\n`); }
|
|
1355
1474
|
// Build request body in parallel with auth resolution. ensureAuth is
|
|
1356
1475
|
// a no-op fast-path on cached tokens, but a refresh round-trip can
|
|
1357
1476
|
// take 300ms+; the body build (message serialisation) overlaps cleanly.
|
|
@@ -1411,6 +1530,26 @@ export class OpenAIOAuthProvider {
|
|
|
1411
1530
|
this._forceHttpFallback = true;
|
|
1412
1531
|
this._forceHttpFallbackUntil = Date.now() + ttlMs;
|
|
1413
1532
|
};
|
|
1533
|
+
const traceWsError = (err, stage = 'primary') => {
|
|
1534
|
+
try {
|
|
1535
|
+
appendAgentTrace({
|
|
1536
|
+
sessionId: poolKey,
|
|
1537
|
+
iteration,
|
|
1538
|
+
kind: 'transport_error',
|
|
1539
|
+
provider: 'openai-oauth',
|
|
1540
|
+
model: useModel,
|
|
1541
|
+
transport: 'websocket',
|
|
1542
|
+
payload: {
|
|
1543
|
+
stage,
|
|
1544
|
+
error_code: err?.code || null,
|
|
1545
|
+
error_http_status: Number(err?.httpStatus || 0) || null,
|
|
1546
|
+
error_classifier: err?.retryClassifier || err?.midstreamClassifier || null,
|
|
1547
|
+
live_text_emitted: err?.liveTextEmitted === true || err?.unsafeToRetry === true,
|
|
1548
|
+
message: String(err?.message || err || '').slice(0, 500),
|
|
1549
|
+
},
|
|
1550
|
+
});
|
|
1551
|
+
} catch {}
|
|
1552
|
+
};
|
|
1414
1553
|
const dispatchHttp = async (reason, originalErr = null, { sticky = false } = {}) => {
|
|
1415
1554
|
appendAgentTrace({
|
|
1416
1555
|
sessionId: poolKey,
|
|
@@ -1433,7 +1572,7 @@ export class OpenAIOAuthProvider {
|
|
|
1433
1572
|
process.stderr.write('[openai-oauth] WebSocket bypassed (forced); using HTTP/SSE\n');
|
|
1434
1573
|
}
|
|
1435
1574
|
} else {
|
|
1436
|
-
process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
|
|
1575
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[openai-oauth] WebSocket unhealthy (${reason}); falling back to HTTP/SSE\n`);
|
|
1437
1576
|
}
|
|
1438
1577
|
const result = await sendHttp({
|
|
1439
1578
|
auth,
|
|
@@ -1481,11 +1620,12 @@ export class OpenAIOAuthProvider {
|
|
|
1481
1620
|
// Prefer WebSocket for hot cache/delta transport; fall back to HTTP/SSE
|
|
1482
1621
|
// after retry-exhausted handshake/acquire/no-first-event failures.
|
|
1483
1622
|
try {
|
|
1484
|
-
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-start model=${useModel}
|
|
1623
|
+
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-start model=${useModel} agent=${_sendAgent} sessionHash=${createHash('sha256').update(String(_sendSessionId)).digest('hex').slice(0, 8)} iteration=${iteration ?? '(none)'}\n`); }
|
|
1485
1624
|
const result = await dispatchWs(false);
|
|
1486
1625
|
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1487
1626
|
return recordLiveModel(result);
|
|
1488
1627
|
} catch (err) {
|
|
1628
|
+
traceWsError(err, 'primary');
|
|
1489
1629
|
const status = err?.httpStatus;
|
|
1490
1630
|
// Live-text invariant: if the WS attempt already relayed a
|
|
1491
1631
|
// non-empty text chunk to the client, NO recovery path may reissue
|
|
@@ -1504,6 +1644,7 @@ export class OpenAIOAuthProvider {
|
|
|
1504
1644
|
if (process.env.MIXDOG_DEBUG_AGENT) { process.stderr.write(`[agent-trace] provider-send-end elapsed=${Date.now() - _t1}ms result=ok\n`); }
|
|
1505
1645
|
return recordLiveModel(result);
|
|
1506
1646
|
} catch (retryErr) {
|
|
1647
|
+
traceWsError(retryErr, 'auth_retry');
|
|
1507
1648
|
if (_shouldUseOpenAIHttpFallback(retryErr, externalSignal)) {
|
|
1508
1649
|
try {
|
|
1509
1650
|
return await dispatchHttp(
|
|
@@ -71,6 +71,24 @@ export class OpenAIDirectProvider {
|
|
|
71
71
|
// so gpt-5.4-mini can opt into Priority even when the OAuth catalog does
|
|
72
72
|
// not advertise a Fast tier for its OAuth endpoint.
|
|
73
73
|
applyOpenAIDirectFastTier(body, useModel, opts);
|
|
74
|
+
// P0 audit fix: buildRequestBody (openai-oauth.mjs) defaults
|
|
75
|
+
// store:false (env-gated, MIXDOG_OAI_STORE), which is correct for
|
|
76
|
+
// the openai-oauth ChatGPT-subscription backend — that backend keeps
|
|
77
|
+
// its own conversation state via the WS handshake session_id
|
|
78
|
+
// (see openai-oauth-ws.mjs "conversation slot ... in-memory prefix
|
|
79
|
+
// state"), independent of the public Responses API `store` field.
|
|
80
|
+
// The public OpenAI direct WS path below, however, talks to the real
|
|
81
|
+
// api.openai.com Responses API, where `previous_response_id`
|
|
82
|
+
// continuation is only valid when the anchored response was actually
|
|
83
|
+
// stored — store:false + previous_response_id is a broken
|
|
84
|
+
// combination there (the server has nothing to look up). This
|
|
85
|
+
// provider's WS transport always injects previous_response_id via
|
|
86
|
+
// openai-oauth-ws.mjs's delta path once a response id is cached, so
|
|
87
|
+
// force store:true here — same override xAI's Responses path takes
|
|
88
|
+
// (see openai-compat.mjs _doSendXaiResponses/_doSendXaiResponsesWebSocket:
|
|
89
|
+
// "the public endpoint currently returns previous_response_not_found
|
|
90
|
+
// ... unless the chain is stored").
|
|
91
|
+
body.store = true;
|
|
74
92
|
// Public Responses API supports prompt_cache_retention='24h' at no
|
|
75
93
|
// extra cost (same cached_input_tokens billing as the default 5–10
|
|
76
94
|
// min in-memory cache). openai-oauth rejects the parameter, so it's
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
2
|
-
import * as fsp from 'node:fs/promises';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
3
|
+
import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
4
4
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
5
5
|
import { getOpenCodeGoAuthCookie } from '../../../shared/config.mjs';
|
|
6
6
|
|
|
@@ -53,12 +53,18 @@ function readJson(file) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Synchronous atomic+lock write (updateJsonAtomicSync) instead of the prior
|
|
57
|
+
// fire-and-forget fsp.writeFile: this cache is single-entry (one snapshot
|
|
58
|
+
// per file, no cross-process merge), so the lock protects against a torn
|
|
59
|
+
// write racing readers, not a lost-update merge. Only one write happens
|
|
60
|
+
// per successful fetch (TTL-gated, at most once per LIVE_TTL_MS), so the
|
|
61
|
+
// switch off async has no meaningful latency impact on the request path.
|
|
56
62
|
function writeJson(file, value) {
|
|
57
|
-
|
|
63
|
+
let next = null;
|
|
58
64
|
try {
|
|
59
|
-
|
|
60
|
-
void fsp.writeFile(file, JSON.stringify(value, null, 2), 'utf8').catch(() => {});
|
|
65
|
+
next = updateJsonAtomicSync(file, () => value, { lock: true, fsyncDir: true, timeoutMs: 1000 }); // best-effort cache write: short lock timeout, don't block on contention
|
|
61
66
|
} catch {}
|
|
67
|
+
if (next) diskJsonCache = { at: Date.now(), file, value: next }; // only mirror on confirmed write, avoid phantom cache on lock timeout
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
function freshSnapshot(snapshot, ttlMs) {
|
|
@@ -201,7 +201,8 @@ export function getProvider(name) {
|
|
|
201
201
|
// unregistered providers default to false (the openai/gemini majority).
|
|
202
202
|
export function providerInputExcludesCache(name) {
|
|
203
203
|
const p = getProvider(name);
|
|
204
|
-
|
|
204
|
+
if (p?.constructor?.inputExcludesCache === true) return true;
|
|
205
|
+
return String(name || '').toLowerCase().includes('anthropic');
|
|
205
206
|
}
|
|
206
207
|
export function getAllProviders() {
|
|
207
208
|
// Defensive copy — callers must not mutate the live registry or retain
|