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
|
@@ -21,35 +21,61 @@ import { enrichModels } from './model-catalog.mjs';
|
|
|
21
21
|
import { makeModelCache } from './model-cache.mjs';
|
|
22
22
|
import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
|
|
23
23
|
import {
|
|
24
|
+
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
24
25
|
PROVIDER_HTTP_RESPONSE_TIMEOUT_MS,
|
|
25
26
|
PROVIDER_RETRY_BACKOFF_MS,
|
|
26
27
|
PROVIDER_RETRY_MAX_ATTEMPTS,
|
|
27
|
-
PROVIDER_SSE_IDLE_TIMEOUT_MS,
|
|
28
28
|
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
29
|
+
PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS,
|
|
30
|
+
streamStalledError,
|
|
29
31
|
createPassthroughSignal,
|
|
30
32
|
} from '../stall-policy.mjs';
|
|
31
33
|
import {
|
|
32
34
|
classifyError,
|
|
35
|
+
classifyMidstreamError,
|
|
33
36
|
midstreamBackoffFor,
|
|
37
|
+
MIDSTREAM_RETRY_POLICY,
|
|
34
38
|
retryAfterMsFromError,
|
|
39
|
+
sleepWithAbort,
|
|
35
40
|
withRetry,
|
|
36
41
|
} from './retry-classifier.mjs';
|
|
37
42
|
import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
|
|
43
|
+
import {
|
|
44
|
+
applyAnthropicEffortToBody,
|
|
45
|
+
effortValuesForModel,
|
|
46
|
+
shouldIncludeEffortBeta,
|
|
47
|
+
} from './anthropic-effort.mjs';
|
|
38
48
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
39
49
|
import { normalizeContentForAnthropic } from './media-normalization.mjs';
|
|
40
50
|
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
51
|
+
import { scanLeakedToolCalls, createToolCallDedupe } from './anthropic-leaked-toolcall.mjs';
|
|
41
52
|
|
|
42
53
|
// --- Model catalog cache helpers ---
|
|
43
54
|
// Disk-backed cache so repeated process starts (cron, tool calls) don't
|
|
44
55
|
// hammer /v1/models. 24h TTL matches the upstream client cadence.
|
|
45
56
|
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
57
|
+
// Bump when the on-disk cache shape changes so stale-shape entries are
|
|
58
|
+
// discarded instead of misread (mirrors openai-oauth's schema-version gate).
|
|
59
|
+
const ANTHROPIC_MODEL_CACHE_SCHEMA_VERSION = 1;
|
|
46
60
|
// SSE progress emits (per-request "Response …" and "Done:" lines). Off by default.
|
|
47
61
|
const SSE_VERBOSE = process.env.MIXDOG_SSE_VERBOSE === '1';
|
|
48
62
|
|
|
49
|
-
/** Bounded mid-stream SSE retries (transient stream loss); shared with anthropic.mjs.
|
|
50
|
-
|
|
63
|
+
/** Bounded mid-stream SSE retries (transient stream loss); shared with anthropic.mjs.
|
|
64
|
+
* Sourced from the single shared retry-budget table (MIDSTREAM_RETRY_POLICY.sse). */
|
|
65
|
+
export const ANTHROPIC_MAX_MIDSTREAM_RETRIES = MIDSTREAM_RETRY_POLICY.sse.defaultRetries;
|
|
66
|
+
|
|
67
|
+
// Policy passed to the shared classifyMidstreamError for the SSE path. The
|
|
68
|
+
// top-of-function attempt-budget gate uses defaultRetries (3); perClassifierGate
|
|
69
|
+
// is false so the classifier returns raw bucket strings (the loop owns the
|
|
70
|
+
// MAX_MIDSTREAM_RETRIES bound), matching the former _classifyMidstreamError.
|
|
71
|
+
const SSE_MIDSTREAM_POLICY = {
|
|
72
|
+
mode: 'sse',
|
|
73
|
+
defaultRetries: MIDSTREAM_RETRY_POLICY.sse.defaultRetries,
|
|
74
|
+
perClassifierGate: false,
|
|
75
|
+
};
|
|
51
76
|
|
|
52
77
|
function formatRetryAfter(ms) {
|
|
78
|
+
if (ms == null) return '';
|
|
53
79
|
const n = Number(ms);
|
|
54
80
|
if (!Number.isFinite(n) || n < 0) return '';
|
|
55
81
|
if (n >= 60_000 && n % 60_000 === 0) return `${Math.round(n / 60_000)}m`;
|
|
@@ -79,6 +105,7 @@ function anthropicQuotaError(status, headers, bodyText = '') {
|
|
|
79
105
|
const _modelCache = makeModelCache({
|
|
80
106
|
fileName: 'anthropic-oauth-models.json',
|
|
81
107
|
ttlMs: MODEL_CACHE_TTL_MS,
|
|
108
|
+
version: ANTHROPIC_MODEL_CACHE_SCHEMA_VERSION,
|
|
82
109
|
onSave: (m) => { _inMemoryCatalog = Array.isArray(m) ? m.slice() : null; },
|
|
83
110
|
});
|
|
84
111
|
|
|
@@ -120,6 +147,10 @@ function _displayModel(id) {
|
|
|
120
147
|
return `claude-${m[1].toLowerCase()}-${m[2]}${m[3] ? `.${m[3]}` : ''}`;
|
|
121
148
|
}
|
|
122
149
|
|
|
150
|
+
function _capabilitySupported(capability) {
|
|
151
|
+
return capability === true || capability?.supported === true;
|
|
152
|
+
}
|
|
153
|
+
|
|
123
154
|
// Classify a model id into our common tier/family shape. Anthropic's catalog
|
|
124
155
|
// mixes dated ids (claude-opus-4-5-20251101), versioned aliases
|
|
125
156
|
// (claude-opus-4-6), and the raw family tokens resolved via env vars.
|
|
@@ -136,15 +167,19 @@ function _normalizeAnthropicModel(raw) {
|
|
|
136
167
|
const releaseDate = dated
|
|
137
168
|
? id.match(/-(\d{4})(\d{2})(\d{2})$/)
|
|
138
169
|
: null;
|
|
170
|
+
const effortValues = effortValuesForModel(raw?.capabilities, id);
|
|
139
171
|
return {
|
|
140
172
|
id,
|
|
141
173
|
display: raw?.display_name || _prettyName(id, family),
|
|
142
174
|
family,
|
|
143
175
|
provider: 'anthropic-oauth',
|
|
144
|
-
contextWindow: raw?.context_window || raw?.max_context_window || _defaultContextForModel(id, family),
|
|
176
|
+
contextWindow: raw?.context_window || raw?.max_context_window || raw?.max_input_tokens || _defaultContextForModel(id, family),
|
|
177
|
+
outputTokens: raw?.max_tokens || raw?.max_output_tokens || null,
|
|
145
178
|
tier,
|
|
146
179
|
latest: false, // assigned in a second pass once full list is known
|
|
147
180
|
releaseDate: releaseDate ? `${releaseDate[1]}-${releaseDate[2]}-${releaseDate[3]}` : null,
|
|
181
|
+
supportsReasoning: effortValues.length > 0 || _capabilitySupported(raw?.capabilities?.thinking),
|
|
182
|
+
reasoningOptions: effortValues.length ? [{ type: 'effort', values: effortValues }] : [],
|
|
148
183
|
};
|
|
149
184
|
}
|
|
150
185
|
|
|
@@ -284,9 +319,11 @@ function resolveCliVersion() {
|
|
|
284
319
|
}
|
|
285
320
|
|
|
286
321
|
function requiresSystemPrefix(model) {
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
322
|
+
// High-tier Claude OAuth models require the first-party system prefix for
|
|
323
|
+
// OAuth pool routing. Haiku does not; keep every other Claude family (Opus,
|
|
324
|
+
// Sonnet, Fable, and future non-Haiku families) on the prefixed path.
|
|
325
|
+
const id = String(model || '').toLowerCase();
|
|
326
|
+
return /^claude-/.test(id) && !/^claude-haiku(?:-|$)/.test(id);
|
|
290
327
|
}
|
|
291
328
|
|
|
292
329
|
// OAuth rate-limit pool routing is gated by the server inspecting the first
|
|
@@ -349,22 +386,77 @@ const MAX_TOKENS = {
|
|
|
349
386
|
'claude-haiku-4-5-20251001': 8192,
|
|
350
387
|
};
|
|
351
388
|
|
|
352
|
-
|
|
389
|
+
// Sanity floor/ceiling for resolveMaxTokens. Catalog-reported outputTokens
|
|
390
|
+
// (from the Anthropic API, cached to disk) can be trusted above these
|
|
391
|
+
// hardcoded fallbacks, but we still clamp to a safety cap so a bad/huge
|
|
392
|
+
// catalog value can't blow the thinking+output budget, and floor so a
|
|
393
|
+
// missing/zero catalog value never degenerates to an unusable cap.
|
|
394
|
+
const MAX_TOKENS_FLOOR = 8192;
|
|
395
|
+
const DEFAULT_SAFETY_CAP = 65536;
|
|
396
|
+
// Parse MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS strictly: only a finite positive
|
|
397
|
+
// number is a valid override. Invalid values ("0", negatives, garbage,
|
|
398
|
+
// whitespace) are treated as unset — NOT as "use the default cap outright" —
|
|
399
|
+
// so resolveMaxTokens still consults catalog/fallback for low-cap models.
|
|
400
|
+
function _envMaxOutputOverride() {
|
|
401
|
+
const raw = process.env.MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS;
|
|
402
|
+
if (raw == null || String(raw).trim() === '') return null;
|
|
403
|
+
const n = Number(raw);
|
|
404
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
405
|
+
return Math.floor(n);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Catalog-reported outputTokens for a model id, read from the in-memory
|
|
409
|
+
// catalog mirror (lazily populated from the disk cache if the mirror hasn't
|
|
410
|
+
// been warmed yet by listModels()). Never throws — any failure just means
|
|
411
|
+
// "no catalog data", and callers fall back to the static heuristics below.
|
|
412
|
+
function _catalogOutputTokens(model) {
|
|
413
|
+
if (!model) return null;
|
|
414
|
+
try {
|
|
415
|
+
if (!Array.isArray(_inMemoryCatalog)) {
|
|
416
|
+
const cached = _modelCache.loadSync();
|
|
417
|
+
if (Array.isArray(cached)) _inMemoryCatalog = cached.slice();
|
|
418
|
+
}
|
|
419
|
+
if (!Array.isArray(_inMemoryCatalog)) return null;
|
|
420
|
+
const entry = _inMemoryCatalog.find(m => m?.id === model);
|
|
421
|
+
const out = Number(entry?.outputTokens);
|
|
422
|
+
return Number.isFinite(out) && out > 0 ? out : null;
|
|
423
|
+
} catch {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function _fallbackMaxTokens(model) {
|
|
353
429
|
if (MAX_TOKENS[model]) return MAX_TOKENS[model];
|
|
354
430
|
const id = String(model || '').toLowerCase();
|
|
355
431
|
if (id.includes('opus')) return 65536;
|
|
432
|
+
if (id.includes('fable')) return 65536;
|
|
433
|
+
// Sonnet 5+ ships a much larger output budget than the legacy 4.x line
|
|
434
|
+
// (this is the claude-sonnet-5 fix: 16384 was starving visible output
|
|
435
|
+
// once extended thinking ate into the same hard cap). Keep sonnet-4-x
|
|
436
|
+
// conservative at 16384; only bump 5+.
|
|
437
|
+
const sonnetVersion = id.match(/^claude-sonnet-(\d+)/);
|
|
438
|
+
if (sonnetVersion) return Number(sonnetVersion[1]) >= 5 ? 65536 : 16384;
|
|
356
439
|
if (id.includes('sonnet')) return 16384;
|
|
357
440
|
if (id.includes('haiku')) return 8192;
|
|
358
441
|
return 8192;
|
|
359
442
|
}
|
|
360
443
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
444
|
+
// resolveMaxTokens: catalog-driven max_tokens for a model id.
|
|
445
|
+
// 1. MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS env override, if set, wins outright.
|
|
446
|
+
// 2. Catalog outputTokens (trusted over hardcoded heuristics when present),
|
|
447
|
+
// clamped to [MAX_TOKENS_FLOOR, safetyCap].
|
|
448
|
+
// 3. Static MAX_TOKENS table / family heuristic fallback when the catalog
|
|
449
|
+
// has no entry for this model, also clamped to the safety cap.
|
|
450
|
+
function resolveMaxTokens(model) {
|
|
451
|
+
const envOverride = _envMaxOutputOverride();
|
|
452
|
+
if (envOverride != null) return envOverride;
|
|
453
|
+
const safetyCap = DEFAULT_SAFETY_CAP;
|
|
454
|
+
const catalogValue = _catalogOutputTokens(model);
|
|
455
|
+
if (catalogValue != null) {
|
|
456
|
+
return Math.max(MAX_TOKENS_FLOOR, Math.min(catalogValue, safetyCap));
|
|
457
|
+
}
|
|
458
|
+
return Math.min(_fallbackMaxTokens(model), safetyCap);
|
|
459
|
+
}
|
|
368
460
|
|
|
369
461
|
const MIN_THINKING_BUDGET = 1024;
|
|
370
462
|
const THINKING_OUTPUT_RESERVE = 1024;
|
|
@@ -378,10 +470,6 @@ function clampThinkingBudgetTokens(value, maxTokens) {
|
|
|
378
470
|
return Math.max(MIN_THINKING_BUDGET, Math.min(desired, ceiling));
|
|
379
471
|
}
|
|
380
472
|
|
|
381
|
-
// Tracks which unknown effort labels we've already logged so a repeated
|
|
382
|
-
// session-level misconfig doesn't flood stderr with the same warning.
|
|
383
|
-
const _LOGGED_UNKNOWN_EFFORT = new Set();
|
|
384
|
-
|
|
385
473
|
// Layered cache TTLs — stable layers get 1h, volatile layers get 5m.
|
|
386
474
|
// Anthropic requires 1h entries to appear before 5m entries in the request.
|
|
387
475
|
const CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' }; // tools, system, tier3, messages
|
|
@@ -889,25 +977,10 @@ function _captureMidstreamAbort(state, reason) {
|
|
|
889
977
|
}
|
|
890
978
|
}
|
|
891
979
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
await new Promise((resolve, reject) => {
|
|
899
|
-
const t = setTimeout(() => {
|
|
900
|
-
try { signal.removeEventListener('abort', onAbort); } catch {}
|
|
901
|
-
resolve();
|
|
902
|
-
}, ms);
|
|
903
|
-
const onAbort = () => {
|
|
904
|
-
clearTimeout(t);
|
|
905
|
-
const reason = signal.reason;
|
|
906
|
-
reject(reason instanceof Error ? reason : new Error('Anthropic OAuth mid-stream retry backoff aborted'));
|
|
907
|
-
};
|
|
908
|
-
if (signal.aborted) { onAbort(); return; }
|
|
909
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
910
|
-
});
|
|
980
|
+
// Abort-aware mid-stream backoff sleep → shared sleepWithAbort
|
|
981
|
+
// (retry-classifier.mjs). abortMessage preserves the prior fallback text.
|
|
982
|
+
function _midstreamSleepWithAbort(ms, signal) {
|
|
983
|
+
return sleepWithAbort(ms, signal, undefined, 'Anthropic OAuth mid-stream retry backoff aborted');
|
|
911
984
|
}
|
|
912
985
|
|
|
913
986
|
function _statusForAnthropicSseError(type, message) {
|
|
@@ -939,10 +1012,23 @@ function _anthropicSseError(event) {
|
|
|
939
1012
|
return err;
|
|
940
1013
|
}
|
|
941
1014
|
|
|
942
|
-
async function parseSSEStream(response, signal, abortStream, onStreamDelta, onToolCall, state, onTextDelta) {
|
|
1015
|
+
async function parseSSEStream(response, signal, abortStream, onStreamDelta, onToolCall, state, onTextDelta, knownToolNames) {
|
|
943
1016
|
const reader = response.body.getReader();
|
|
944
1017
|
const decoder = new TextDecoder();
|
|
945
|
-
|
|
1018
|
+
// SEMANTIC idle window: reset only by real model events (message/content/
|
|
1019
|
+
// tool deltas), NOT by raw keepalive bytes. A ping-only wedge therefore
|
|
1020
|
+
// trips this within the window instead of hanging until the 30-min agent
|
|
1021
|
+
// watchdog. See resetIdleTimer + the per-event reset in the loop below.
|
|
1022
|
+
// state.semanticIdleTimeoutMs is a test/override seam (same shape as
|
|
1023
|
+
// firstMessageTimeoutMs); production uses the shared env-backed default.
|
|
1024
|
+
const SSE_IDLE_TIMEOUT_MS = Number.isFinite(Number(state?.semanticIdleTimeoutMs))
|
|
1025
|
+
&& Number(state.semanticIdleTimeoutMs) > 0
|
|
1026
|
+
? Number(state.semanticIdleTimeoutMs)
|
|
1027
|
+
: PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
|
|
1028
|
+
const SSE_FIRST_MESSAGE_TIMEOUT_MS = Number.isFinite(Number(state?.firstMessageTimeoutMs))
|
|
1029
|
+
&& Number(state.firstMessageTimeoutMs) > 0
|
|
1030
|
+
? Number(state.firstMessageTimeoutMs)
|
|
1031
|
+
: PROVIDER_FIRST_BYTE_TIMEOUT_MS;
|
|
946
1032
|
let content = '';
|
|
947
1033
|
let hasThinkingContent = false;
|
|
948
1034
|
const contentBlockTypes = new Set();
|
|
@@ -952,16 +1038,136 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
952
1038
|
let stopReason = null;
|
|
953
1039
|
let buffer = '';
|
|
954
1040
|
let idleTimedOut = false;
|
|
1041
|
+
let firstMessageTimedOut = false;
|
|
955
1042
|
let idleTimer = null;
|
|
1043
|
+
let firstMessageTimer = null;
|
|
956
1044
|
let currentEvent = '';
|
|
957
1045
|
|
|
958
1046
|
const pendingToolInputs = new Map();
|
|
959
1047
|
|
|
1048
|
+
// Leaked tool-call guard. The model (esp. Opus via OAuth) occasionally
|
|
1049
|
+
// emits a tool call as plain text tags inside `text_delta` instead of a
|
|
1050
|
+
// native `tool_use` block. `leakBuffer` is a minimal rolling window that
|
|
1051
|
+
// only holds back text when a partial sentinel prefix is present, so a
|
|
1052
|
+
// tag split across chunk boundaries is still detected while ordinary text
|
|
1053
|
+
// still streams promptly. The guard is additive: the native tool_use path
|
|
1054
|
+
// (content_block_start/input_json_delta/content_block_stop) is untouched.
|
|
1055
|
+
const _knownTools = knownToolNames instanceof Set
|
|
1056
|
+
? knownToolNames
|
|
1057
|
+
: new Set(Array.isArray(knownToolNames) ? knownToolNames : []);
|
|
1058
|
+
const _leakGuardEnabled = _knownTools.size > 0;
|
|
1059
|
+
const _isKnownTool = (name) => _knownTools.has(name);
|
|
1060
|
+
let leakBuffer = '';
|
|
1061
|
+
// Running markdown fence/inline-code state threaded across text_delta
|
|
1062
|
+
// chunks (Fix 1): a tool-call tag inside a ```code fence``` or inline span
|
|
1063
|
+
// is a doc example, not a real call — the guard emits it as text.
|
|
1064
|
+
let leakFenceState = undefined;
|
|
1065
|
+
// Cross-path fingerprint dedupe (Fix 2): a synthesized text-leaked call and
|
|
1066
|
+
// an identical native tool_use block must dispatch onToolCall exactly once.
|
|
1067
|
+
const _toolDedupe = createToolCallDedupe();
|
|
1068
|
+
|
|
1069
|
+
// Synthesize + dispatch a recovered leaked call exactly like the native
|
|
1070
|
+
// content_block_stop path (push into toolCalls, flag state, eager
|
|
1071
|
+
// onToolCall). A generated id mirrors the `toolu_`-prefixed native shape.
|
|
1072
|
+
const dispatchLeakedCall = (recovered) => {
|
|
1073
|
+
let args = recovered?.arguments;
|
|
1074
|
+
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
1075
|
+
// Skip if an identical native (or prior synthetic) call already fired.
|
|
1076
|
+
if (!_toolDedupe.shouldDispatch(recovered.name, args)) return;
|
|
1077
|
+
const call = {
|
|
1078
|
+
id: `toolu_leaked_${randomBytes(8).toString('hex')}`,
|
|
1079
|
+
name: recovered.name,
|
|
1080
|
+
arguments: args,
|
|
1081
|
+
};
|
|
1082
|
+
toolCalls.push(call);
|
|
1083
|
+
if (state) state.emittedToolCall = true;
|
|
1084
|
+
try { onToolCall?.(call); } catch {}
|
|
1085
|
+
try { onStreamDelta?.(); } catch {}
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
// Feed accumulated text through the scanner. On `final` nothing is held
|
|
1089
|
+
// back so legitimate text is never lost at stream end.
|
|
1090
|
+
const pumpLeakBuffer = (final) => {
|
|
1091
|
+
if (!_leakGuardEnabled) return;
|
|
1092
|
+
if (!leakBuffer && !final) return;
|
|
1093
|
+
const { emit, calls, rest, fenceState } = scanLeakedToolCalls(leakBuffer, { isKnownTool: _isKnownTool, final, fenceState: leakFenceState });
|
|
1094
|
+
leakBuffer = rest;
|
|
1095
|
+
leakFenceState = fenceState;
|
|
1096
|
+
if (emit) {
|
|
1097
|
+
content += emit;
|
|
1098
|
+
if (onTextDelta) {
|
|
1099
|
+
if (state) state.emittedText = true;
|
|
1100
|
+
try { onTextDelta(emit); } catch {}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
for (const c of calls) dispatchLeakedCall(c);
|
|
1104
|
+
};
|
|
1105
|
+
|
|
960
1106
|
// Holds the in-flight reader.read() race rejector so the idle timer can
|
|
961
1107
|
// force-unblock the loop even when reader.cancel() fails to settle the
|
|
962
1108
|
// pending read (undici half-open socket). See resetIdleTimer below.
|
|
963
1109
|
let idleReject = null;
|
|
964
1110
|
|
|
1111
|
+
const firstMessageTimeoutError = () => {
|
|
1112
|
+
const err = new Error(`Anthropic OAuth SSE stream produced no message_start within ${SSE_FIRST_MESSAGE_TIMEOUT_MS}ms`);
|
|
1113
|
+
err.code = 'EEMPTYSTREAM';
|
|
1114
|
+
err.isEmptyStream = true;
|
|
1115
|
+
err.firstByteTimeout = true;
|
|
1116
|
+
return err;
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
const clearFirstMessageTimer = () => {
|
|
1120
|
+
if (firstMessageTimer) {
|
|
1121
|
+
clearTimeout(firstMessageTimer);
|
|
1122
|
+
firstMessageTimer = null;
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
const armFirstMessageTimer = () => {
|
|
1127
|
+
if (!(SSE_FIRST_MESSAGE_TIMEOUT_MS > 0)) return;
|
|
1128
|
+
clearFirstMessageTimer();
|
|
1129
|
+
firstMessageTimer = setTimeout(() => {
|
|
1130
|
+
if (state?.sawMessageStart) return;
|
|
1131
|
+
firstMessageTimedOut = true;
|
|
1132
|
+
const err = firstMessageTimeoutError();
|
|
1133
|
+
try { abortStream?.(err); } catch (abortErr) {
|
|
1134
|
+
try { process.stderr.write(`[anthropic-oauth] sse first-message abortStream failed: ${abortErr?.message ?? String(abortErr)}\n`); } catch {}
|
|
1135
|
+
}
|
|
1136
|
+
try {
|
|
1137
|
+
const _c = reader.cancel('SSE first message timeout');
|
|
1138
|
+
if (_c && typeof _c.catch === 'function') _c.catch(() => {});
|
|
1139
|
+
} catch (cancelErr) {
|
|
1140
|
+
try { process.stderr.write(`[anthropic-oauth] sse first-message cancel failed: ${cancelErr?.message ?? String(cancelErr)}\n`); } catch {}
|
|
1141
|
+
}
|
|
1142
|
+
if (idleReject) {
|
|
1143
|
+
const r = idleReject; idleReject = null; r(err);
|
|
1144
|
+
}
|
|
1145
|
+
}, SSE_FIRST_MESSAGE_TIMEOUT_MS);
|
|
1146
|
+
try { firstMessageTimer.unref?.(); } catch {}
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
// Attach the partial stream state to a mid-stream stall error so the agent
|
|
1150
|
+
// loop can decide SUCCESS vs FAILURE. The recurring "worker finished but
|
|
1151
|
+
// owner never notified" case is a FINAL no-tool summary stream that wedges
|
|
1152
|
+
// ping-only after the real work (tool calls) already completed in earlier
|
|
1153
|
+
// iterations: there is streamed `content`, no pending tool_use, and no
|
|
1154
|
+
// emitted tool call this iteration. The loop treats that as a successful
|
|
1155
|
+
// partial-final (deliver the summary we have) instead of dropping it. A
|
|
1156
|
+
// stall WITH a pending/emitted tool call stays a hard failure (a tool whose
|
|
1157
|
+
// input never completed must never be reported as done).
|
|
1158
|
+
const _attachStallPartial = (err) => {
|
|
1159
|
+
try {
|
|
1160
|
+
err.partialContent = content;
|
|
1161
|
+
err.partialToolCalls = toolCalls.length ? toolCalls.slice() : undefined;
|
|
1162
|
+
err.pendingToolUse = pendingToolInputs.size > 0;
|
|
1163
|
+
err.partialModel = model || undefined;
|
|
1164
|
+
err.partialUsage = usage;
|
|
1165
|
+
err.partialStopReason = stopReason || undefined;
|
|
1166
|
+
err.partialHasThinking = hasThinkingContent;
|
|
1167
|
+
} catch { /* best-effort enrichment */ }
|
|
1168
|
+
return err;
|
|
1169
|
+
};
|
|
1170
|
+
|
|
965
1171
|
const resetIdleTimer = () => {
|
|
966
1172
|
// OFF by default. When disabled the
|
|
967
1173
|
// idle timer never arms, so the stream is never killed on inactivity;
|
|
@@ -984,13 +1190,14 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
984
1190
|
// pending forever and the SSE idle timeout never unblocks the loop —
|
|
985
1191
|
// the 391s-hang root cause.
|
|
986
1192
|
if (idleReject) {
|
|
987
|
-
const e =
|
|
988
|
-
e.code = 'ETIMEDOUT';
|
|
1193
|
+
const e = _attachStallPartial(streamStalledError('Anthropic OAuth SSE', SSE_IDLE_TIMEOUT_MS, { emittedToolCall: !!state?.emittedToolCall }));
|
|
989
1194
|
const r = idleReject; idleReject = null; r(e);
|
|
990
1195
|
}
|
|
991
|
-
// Shared provider policy: short
|
|
992
|
-
//
|
|
1196
|
+
// Shared provider policy: short SEMANTIC-event inactivity catches the
|
|
1197
|
+
// ping-only wedge where SSE starts, emits some deltas, then goes silent
|
|
1198
|
+
// while `:ping` keepalives keep the transport socket warm.
|
|
993
1199
|
}, SSE_IDLE_TIMEOUT_MS);
|
|
1200
|
+
try { idleTimer.unref?.(); } catch {}
|
|
994
1201
|
};
|
|
995
1202
|
|
|
996
1203
|
const onAbort = () => {
|
|
@@ -1010,7 +1217,13 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1010
1217
|
}
|
|
1011
1218
|
|
|
1012
1219
|
try {
|
|
1013
|
-
|
|
1220
|
+
// Part A / reviewer fix: do NOT arm the SEMANTIC idle timer before the
|
|
1221
|
+
// stream has produced its first event. A slow first response is governed
|
|
1222
|
+
// by armFirstMessageTimer() (first-byte window) alone; arming the
|
|
1223
|
+
// semantic idle here could let it win and mis-abort a legitimately slow
|
|
1224
|
+
// first response as a stall. The semantic idle is first armed at
|
|
1225
|
+
// `message_start` (see below), so it only ever guards MID-stream silence.
|
|
1226
|
+
armFirstMessageTimer();
|
|
1014
1227
|
streamLoop: while (true) {
|
|
1015
1228
|
let chunk;
|
|
1016
1229
|
try {
|
|
@@ -1022,9 +1235,10 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1022
1235
|
});
|
|
1023
1236
|
} catch (err) {
|
|
1024
1237
|
if (idleTimedOut) {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1238
|
+
throw _attachStallPartial(streamStalledError('Anthropic OAuth SSE', SSE_IDLE_TIMEOUT_MS, { emittedToolCall: !!state?.emittedToolCall }));
|
|
1239
|
+
}
|
|
1240
|
+
if (firstMessageTimedOut) {
|
|
1241
|
+
throw firstMessageTimeoutError();
|
|
1028
1242
|
}
|
|
1029
1243
|
if (signal?.aborted) {
|
|
1030
1244
|
_captureMidstreamAbort(state, signal.reason);
|
|
@@ -1037,21 +1251,25 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1037
1251
|
const { done, value } = chunk;
|
|
1038
1252
|
if (done) break;
|
|
1039
1253
|
|
|
1040
|
-
resetIdleTimer();
|
|
1041
1254
|
buffer += decoder.decode(value, { stream: true });
|
|
1042
1255
|
const lines = buffer.split('\n');
|
|
1043
1256
|
buffer = lines.pop() || '';
|
|
1044
1257
|
|
|
1045
1258
|
for (const line of lines) {
|
|
1046
1259
|
if (line.startsWith(':')) {
|
|
1047
|
-
// SSE comment frame (Anthropic `:ping` keepalive).
|
|
1048
|
-
//
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
// the
|
|
1052
|
-
|
|
1260
|
+
// SSE comment frame (Anthropic `:ping` keepalive). Keep it
|
|
1261
|
+
// at transport level only: comments must not refresh the
|
|
1262
|
+
// agent's semantic progress timestamp, or a ping-only 200
|
|
1263
|
+
// can look alive forever without message_start/content.
|
|
1264
|
+
// Crucially it also does NOT reset the SEMANTIC idle timer
|
|
1265
|
+
// below — a ping-only wedge must trip the idle abort.
|
|
1053
1266
|
continue;
|
|
1054
1267
|
}
|
|
1268
|
+
// Blank lines are SSE record separators — emitted after EVERY
|
|
1269
|
+
// frame, including `:ping` keepalives — so they are NOT semantic
|
|
1270
|
+
// progress and must not reset the idle timer (else a ping frame's
|
|
1271
|
+
// trailing blank would keep a wedge alive forever).
|
|
1272
|
+
if (line === '') continue;
|
|
1055
1273
|
if (line.startsWith('event: ')) {
|
|
1056
1274
|
currentEvent = line.slice(7).trim();
|
|
1057
1275
|
continue;
|
|
@@ -1063,11 +1281,24 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1063
1281
|
try {
|
|
1064
1282
|
const event = JSON.parse(data);
|
|
1065
1283
|
|
|
1284
|
+
// SEMANTIC idle reset (Part A): reset the idle timer ONLY for
|
|
1285
|
+
// real progress events, NOT for Anthropic keepalives. Anthropic
|
|
1286
|
+
// sends pings as a NAMED event (`event: ping` /
|
|
1287
|
+
// `data: {"type":"ping"}`), not just `:` comment frames, so a
|
|
1288
|
+
// named ping must be excluded here or a ping-only wedge keeps
|
|
1289
|
+
// the timer alive forever. Everything that is not a ping is a
|
|
1290
|
+
// genuine server event (message_start/content/tool/thinking
|
|
1291
|
+
// deltas, message_delta/stop, errors) and counts as progress.
|
|
1292
|
+
if (currentEvent !== 'ping' && event?.type !== 'ping') {
|
|
1293
|
+
resetIdleTimer();
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1066
1296
|
if (currentEvent === 'error' || event?.type === 'error' || event?.error) {
|
|
1067
1297
|
throw _anthropicSseError(event);
|
|
1068
1298
|
}
|
|
1069
1299
|
|
|
1070
1300
|
if (event.type === 'message_start' && event.message) {
|
|
1301
|
+
clearFirstMessageTimer();
|
|
1071
1302
|
if (state) state.sawMessageStart = true;
|
|
1072
1303
|
if (event.message.model) model = event.message.model;
|
|
1073
1304
|
if (event.message.usage) {
|
|
@@ -1092,8 +1323,13 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1092
1323
|
if (event.type === 'content_block_delta') {
|
|
1093
1324
|
const delta = event.delta;
|
|
1094
1325
|
if (delta?.type) contentBlockTypes.add(delta.type);
|
|
1326
|
+
// Time-to-first-token: stamp the first content delta
|
|
1327
|
+
// (text / thinking / tool input_json) exactly once so
|
|
1328
|
+
// the SSE trace can separate first-byte latency from
|
|
1329
|
+
// total stream/generation time. Without this stamp
|
|
1330
|
+
// ttftMs was always null and reported as 0ms.
|
|
1331
|
+
if (state && !state.ttftAt) state.ttftAt = Date.now();
|
|
1095
1332
|
if (delta?.type === 'text_delta') {
|
|
1096
|
-
content += delta.text || '';
|
|
1097
1333
|
try { onStreamDelta?.(); } catch {}
|
|
1098
1334
|
// Live text relay (gateway): forward the explicit
|
|
1099
1335
|
// text chunk. thinking/signature/input_json deltas
|
|
@@ -1102,9 +1338,21 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1102
1338
|
// live it cannot be withdrawn, so flag the attempt so
|
|
1103
1339
|
// the mid-stream retry loop treats any later failure
|
|
1104
1340
|
// as final (a retry would concatenate attempts).
|
|
1105
|
-
if (
|
|
1106
|
-
|
|
1107
|
-
|
|
1341
|
+
if (_leakGuardEnabled) {
|
|
1342
|
+
// Route text through the leaked-tool-call guard.
|
|
1343
|
+
// It appends to `content`, forwards visible text
|
|
1344
|
+
// via onTextDelta, and synthesizes/dispatches any
|
|
1345
|
+
// recovered known-tool call — suppressing the
|
|
1346
|
+
// tags from the visible stream. A partial sentinel
|
|
1347
|
+
// is held in leakBuffer until the next chunk.
|
|
1348
|
+
leakBuffer += delta.text || '';
|
|
1349
|
+
pumpLeakBuffer(false);
|
|
1350
|
+
} else {
|
|
1351
|
+
content += delta.text || '';
|
|
1352
|
+
if (delta.text && onTextDelta) {
|
|
1353
|
+
if (state) state.emittedText = true;
|
|
1354
|
+
try { onTextDelta(delta.text); } catch {}
|
|
1355
|
+
}
|
|
1108
1356
|
}
|
|
1109
1357
|
}
|
|
1110
1358
|
if (delta?.type === 'thinking_delta' || delta?.type === 'signature_delta') {
|
|
@@ -1160,13 +1408,25 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1160
1408
|
name: pending.name,
|
|
1161
1409
|
arguments: parsedArgs,
|
|
1162
1410
|
};
|
|
1163
|
-
toolCalls.push(call);
|
|
1164
1411
|
pendingToolInputs.delete(event.index);
|
|
1165
|
-
if (state) state.emittedToolCall = true;
|
|
1166
1412
|
// Eager dispatch: let the loop start this tool
|
|
1167
1413
|
// before message_stop arrives. The loop keys
|
|
1168
1414
|
// pending promises by call.id so order is safe.
|
|
1169
|
-
|
|
1415
|
+
// Fix 2: skip the ENTIRE call (push + dispatch) when a
|
|
1416
|
+
// text-leaked synthetic of the same (name,args) already
|
|
1417
|
+
// fired — otherwise the duplicate stays in `toolCalls`
|
|
1418
|
+
// and the loop executes the side-effecting tool twice.
|
|
1419
|
+
// An invalid-args marker never fingerprint-collides with
|
|
1420
|
+
// a real recovered call, so malformed native calls still
|
|
1421
|
+
// dispatch (the marker path is unaffected).
|
|
1422
|
+
if (_toolDedupe.shouldDispatch(call.name, call.arguments)) {
|
|
1423
|
+
toolCalls.push(call);
|
|
1424
|
+
if (state) state.emittedToolCall = true;
|
|
1425
|
+
// Eager dispatch: let the loop start this tool
|
|
1426
|
+
// before message_stop arrives. The loop keys
|
|
1427
|
+
// pending promises by call.id so order is safe.
|
|
1428
|
+
try { onToolCall?.(call); } catch {}
|
|
1429
|
+
}
|
|
1170
1430
|
try { onStreamDelta?.(); } catch {}
|
|
1171
1431
|
}
|
|
1172
1432
|
}
|
|
@@ -1207,6 +1467,12 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1207
1467
|
}
|
|
1208
1468
|
}
|
|
1209
1469
|
|
|
1470
|
+
// Stream ended: flush any held-back leaked-tool-call buffer. `final`
|
|
1471
|
+
// holds nothing back, so a trailing partial sentinel that never
|
|
1472
|
+
// resolved into a real call is surfaced as ordinary text — legitimate
|
|
1473
|
+
// user-visible content is never lost on the failure path.
|
|
1474
|
+
pumpLeakBuffer(true);
|
|
1475
|
+
|
|
1210
1476
|
// Truncated-stream guard: if the reader loop exited (EOF or break)
|
|
1211
1477
|
// after message_start but without seeing message_stop / a tool_use
|
|
1212
1478
|
// stop_reason, the assistant turn was cut off mid-flight. Returning
|
|
@@ -1243,6 +1509,7 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1243
1509
|
};
|
|
1244
1510
|
} finally {
|
|
1245
1511
|
if (idleTimer) clearTimeout(idleTimer);
|
|
1512
|
+
clearFirstMessageTimer();
|
|
1246
1513
|
if (signal) signal.removeEventListener('abort', onAbort);
|
|
1247
1514
|
try { reader.releaseLock(); } catch (err) {
|
|
1248
1515
|
try { process.stderr.write(`[anthropic-oauth] reader releaseLock failed: ${err?.message ?? String(err)}\n`); } catch {}
|
|
@@ -1258,35 +1525,14 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1258
1525
|
* That keeps recovery limited to transport/stream stalls without risking
|
|
1259
1526
|
* duplicate eager tool execution.
|
|
1260
1527
|
*/
|
|
1528
|
+
// Thin wrapper: the SSE mid-stream decision tree now lives in the shared
|
|
1529
|
+
// classifyMidstreamError (retry-classifier.mjs, policy.mode='sse'). Kept as a
|
|
1530
|
+
// named export so internal call sites AND anthropic.mjs (which imports this
|
|
1531
|
+
// symbol) keep resolving it. Behavior is byte-identical — the shared function
|
|
1532
|
+
// is the relocated original, gated by SSE_MIDSTREAM_POLICY (defaultRetries=3,
|
|
1533
|
+
// perClassifierGate:false).
|
|
1261
1534
|
export function _classifyMidstreamError(err, state) {
|
|
1262
|
-
|
|
1263
|
-
if ((state.attemptIndex | 0) >= ANTHROPIC_MAX_MIDSTREAM_RETRIES) return null;
|
|
1264
|
-
if (state.sawCompleted) return null;
|
|
1265
|
-
if (!state.sawMessageStart) return null;
|
|
1266
|
-
if (state.userAbort) return null;
|
|
1267
|
-
if (state.emittedToolCall) return null;
|
|
1268
|
-
|
|
1269
|
-
if (!err) return null;
|
|
1270
|
-
const status = Number(err?.httpStatus || 0);
|
|
1271
|
-
if (status === 401 || status === 403 || status === 429) return null;
|
|
1272
|
-
|
|
1273
|
-
const name = err?.name || '';
|
|
1274
|
-
if (name === 'AgentStallAbortError') return 'agent_stall';
|
|
1275
|
-
if (name === 'StreamStalledAbortError') return 'stream_stalled';
|
|
1276
|
-
if (state.watchdogAbort === 'AgentStallAbortError') return 'agent_stall';
|
|
1277
|
-
if (state.watchdogAbort === 'StreamStalledAbortError') return 'stream_stalled';
|
|
1278
|
-
|
|
1279
|
-
const code = err?.code || err?.cause?.code || '';
|
|
1280
|
-
if (code === 'ECONNRESET') return 'reset';
|
|
1281
|
-
if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout';
|
|
1282
|
-
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'EAI_NODATA') return 'dns';
|
|
1283
|
-
|
|
1284
|
-
const msg = String(err?.message || '').toLowerCase();
|
|
1285
|
-
if (msg.includes('stream timed out after') && msg.includes('of inactivity')) return 'sse_idle_timeout';
|
|
1286
|
-
if (msg.includes('body stream') && msg.includes('terminated')) return 'stream_terminated';
|
|
1287
|
-
if (msg.includes('fetch failed')) return 'fetch_failed';
|
|
1288
|
-
|
|
1289
|
-
return null;
|
|
1535
|
+
return classifyMidstreamError(err, state, SSE_MIDSTREAM_POLICY);
|
|
1290
1536
|
}
|
|
1291
1537
|
|
|
1292
1538
|
// --- Build request body ---
|
|
@@ -1387,21 +1633,13 @@ function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
1387
1633
|
body.tools = [...nativeTools, ...toAnthropicTools([...(tools || []), ...deferredTools])];
|
|
1388
1634
|
}
|
|
1389
1635
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
if (budgetTokens) body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
|
|
1398
|
-
} else if (!_LOGGED_UNKNOWN_EFFORT.has(opts.effort)) {
|
|
1399
|
-
_LOGGED_UNKNOWN_EFFORT.add(opts.effort);
|
|
1400
|
-
try {
|
|
1401
|
-
process.stderr.write(`[anthropic-oauth] unknown effort=${opts.effort} ignored (known: ${Object.keys(EFFORT_BUDGET).join(',')})\n`);
|
|
1402
|
-
} catch {}
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1636
|
+
applyAnthropicEffortToBody(body, {
|
|
1637
|
+
model,
|
|
1638
|
+
opts,
|
|
1639
|
+
maxTokens,
|
|
1640
|
+
clampThinkingBudgetTokens,
|
|
1641
|
+
logTag: 'anthropic-oauth',
|
|
1642
|
+
});
|
|
1405
1643
|
|
|
1406
1644
|
if (opts.fast === true && supportsAnthropicFastMode(model)) {
|
|
1407
1645
|
body.speed = 'fast';
|
|
@@ -1552,6 +1790,15 @@ export class AnthropicOAuthProvider {
|
|
|
1552
1790
|
if (body.speed === 'fast') {
|
|
1553
1791
|
this.fastModeBetaHeaderLatched = true;
|
|
1554
1792
|
}
|
|
1793
|
+
// Known tool names for the leaked-tool-call guard in parseSSEStream:
|
|
1794
|
+
// recovered leaked calls are only synthesized when they name a tool
|
|
1795
|
+
// actually offered to this request (native + lowered). Derived from the
|
|
1796
|
+
// final request body so it matches exactly what the model was given.
|
|
1797
|
+
const knownToolNames = new Set(
|
|
1798
|
+
(Array.isArray(body.tools) ? body.tools : [])
|
|
1799
|
+
.map((t) => (t && typeof t.name === 'string' ? t.name : null))
|
|
1800
|
+
.filter(Boolean),
|
|
1801
|
+
);
|
|
1555
1802
|
const sessionId = opts.sessionId || null;
|
|
1556
1803
|
const iteration = Number.isFinite(Number(opts.iteration)) ? Number(opts.iteration) : null;
|
|
1557
1804
|
// Option A: no absolute wall-clock cap on streaming generation. A stream
|
|
@@ -1620,6 +1867,7 @@ export class AnthropicOAuthProvider {
|
|
|
1620
1867
|
base: OAUTH_BETA_HEADERS,
|
|
1621
1868
|
fastMode: this.fastModeBetaHeaderLatched,
|
|
1622
1869
|
toolSearch: true,
|
|
1870
|
+
effort: shouldIncludeEffortBeta(useModel, opts),
|
|
1623
1871
|
}),
|
|
1624
1872
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
1625
1873
|
'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
|
|
@@ -1780,11 +2028,12 @@ export class AnthropicOAuthProvider {
|
|
|
1780
2028
|
const result = await parseSSEFn(
|
|
1781
2029
|
response,
|
|
1782
2030
|
controller.signal,
|
|
1783
|
-
() => controller.abort(),
|
|
2031
|
+
(reason) => controller.abort(reason),
|
|
1784
2032
|
onStreamDelta,
|
|
1785
2033
|
onToolCall,
|
|
1786
2034
|
midState,
|
|
1787
2035
|
onTextDelta,
|
|
2036
|
+
knownToolNames,
|
|
1788
2037
|
);
|
|
1789
2038
|
|
|
1790
2039
|
const ttftMs = midState.ttftAt ? midState.ttftAt - sseStartedAt : null;
|
|
@@ -2236,3 +2485,7 @@ export async function loginOAuth() {
|
|
|
2236
2485
|
// Lets the SSE parser be exercised in isolation against a synthetic
|
|
2237
2486
|
// ReadableStream without needing a live OAuth session.
|
|
2238
2487
|
export { parseSSEStream };
|
|
2488
|
+
|
|
2489
|
+
// Test-only escape hatch for scripts/tool-smoke.mjs to verify the
|
|
2490
|
+
// catalog-driven max-tokens resolution without duplicating its logic.
|
|
2491
|
+
export const _test = { resolveMaxTokens };
|