mixdog 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -23
- package/package.json +34 -28
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +205 -33
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1222 -90
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +108 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- 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 +519 -118
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +104 -473
- 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 +44 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +139 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -95
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +428 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +756 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/ink.js +54 -8
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- 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/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -3,18 +3,17 @@
|
|
|
3
3
|
* for Claude Max subscription access.
|
|
4
4
|
*
|
|
5
5
|
* Raw HTTP + SSE streaming, reuses message/tool conversion patterns
|
|
6
|
-
* from anthropic.mjs.
|
|
6
|
+
* from anthropic.mjs. agent-trace instrumented.
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
|
-
import { homedir } from 'os';
|
|
11
10
|
import { createServer } from 'http';
|
|
12
11
|
import { randomBytes, createHash } from 'crypto';
|
|
13
12
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from '../
|
|
13
|
+
traceAgentFetch,
|
|
14
|
+
traceAgentSse,
|
|
15
|
+
traceAgentUsage,
|
|
16
|
+
} from '../agent-trace.mjs';
|
|
18
17
|
import { createAbortController } from '../../../shared/abort-controller.mjs';
|
|
19
18
|
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
20
19
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
@@ -31,12 +30,17 @@ import {
|
|
|
31
30
|
} from '../stall-policy.mjs';
|
|
32
31
|
import {
|
|
33
32
|
classifyError,
|
|
33
|
+
classifyMidstreamError,
|
|
34
|
+
midstreamBackoffFor,
|
|
35
|
+
MIDSTREAM_RETRY_POLICY,
|
|
34
36
|
retryAfterMsFromError,
|
|
37
|
+
sleepWithAbort,
|
|
35
38
|
withRetry,
|
|
36
39
|
} from './retry-classifier.mjs';
|
|
37
40
|
import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
|
|
38
41
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
39
42
|
import { normalizeContentForAnthropic } from './media-normalization.mjs';
|
|
43
|
+
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
40
44
|
|
|
41
45
|
// --- Model catalog cache helpers ---
|
|
42
46
|
// Disk-backed cache so repeated process starts (cron, tool calls) don't
|
|
@@ -45,6 +49,47 @@ const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
|
45
49
|
// SSE progress emits (per-request "Response …" and "Done:" lines). Off by default.
|
|
46
50
|
const SSE_VERBOSE = process.env.MIXDOG_SSE_VERBOSE === '1';
|
|
47
51
|
|
|
52
|
+
/** Bounded mid-stream SSE retries (transient stream loss); shared with anthropic.mjs.
|
|
53
|
+
* Sourced from the single shared retry-budget table (MIDSTREAM_RETRY_POLICY.sse). */
|
|
54
|
+
export const ANTHROPIC_MAX_MIDSTREAM_RETRIES = MIDSTREAM_RETRY_POLICY.sse.defaultRetries;
|
|
55
|
+
|
|
56
|
+
// Policy passed to the shared classifyMidstreamError for the SSE path. The
|
|
57
|
+
// top-of-function attempt-budget gate uses defaultRetries (3); perClassifierGate
|
|
58
|
+
// is false so the classifier returns raw bucket strings (the loop owns the
|
|
59
|
+
// MAX_MIDSTREAM_RETRIES bound), matching the former _classifyMidstreamError.
|
|
60
|
+
const SSE_MIDSTREAM_POLICY = {
|
|
61
|
+
mode: 'sse',
|
|
62
|
+
defaultRetries: MIDSTREAM_RETRY_POLICY.sse.defaultRetries,
|
|
63
|
+
perClassifierGate: false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function formatRetryAfter(ms) {
|
|
67
|
+
const n = Number(ms);
|
|
68
|
+
if (!Number.isFinite(n) || n < 0) return '';
|
|
69
|
+
if (n >= 60_000 && n % 60_000 === 0) return `${Math.round(n / 60_000)}m`;
|
|
70
|
+
if (n >= 1000) return `${Math.ceil(n / 1000)}s`;
|
|
71
|
+
return `${Math.ceil(n)}ms`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function anthropicQuotaError(status, headers, bodyText = '') {
|
|
75
|
+
const retryAfterMs = retryAfterMsFromError({ headers, response: { headers } });
|
|
76
|
+
const retryAfter = formatRetryAfter(retryAfterMs);
|
|
77
|
+
const detail = bodyText ? `: ${String(bodyText).slice(0, 200)}` : '';
|
|
78
|
+
const retry = retryAfter ? ` retryAfter=${retryAfter}` : '';
|
|
79
|
+
const err = new Error(`Anthropic OAuth API ${status} quota/rate limit${retry}${detail}`);
|
|
80
|
+
err.name = 'ProviderQuotaError';
|
|
81
|
+
err.code = 'PROVIDER_QUOTA';
|
|
82
|
+
err.httpStatus = status;
|
|
83
|
+
err.status = status;
|
|
84
|
+
err.headers = headers;
|
|
85
|
+
err.response = { status, headers };
|
|
86
|
+
err.retryAfterMs = retryAfterMs;
|
|
87
|
+
err.providerQuota = true;
|
|
88
|
+
err.quotaExceeded = true;
|
|
89
|
+
err.unsafeToRetry = true;
|
|
90
|
+
return err;
|
|
91
|
+
}
|
|
92
|
+
|
|
48
93
|
const _modelCache = makeModelCache({
|
|
49
94
|
fileName: 'anthropic-oauth-models.json',
|
|
50
95
|
ttlMs: MODEL_CACHE_TTL_MS,
|
|
@@ -215,10 +260,9 @@ function assertSafeTokenURL(rawURL) {
|
|
|
215
260
|
}
|
|
216
261
|
return rawURL;
|
|
217
262
|
}
|
|
218
|
-
const TOKEN_URL = assertSafeTokenURL(process.env.ANTHROPIC_OAUTH_TOKEN_URL || 'https://
|
|
263
|
+
const TOKEN_URL = assertSafeTokenURL(process.env.ANTHROPIC_OAUTH_TOKEN_URL || 'https://platform.claude.com/v1/oauth/token');
|
|
219
264
|
const ANTHROPIC_VERSION = '2023-06-01';
|
|
220
265
|
const DEFAULT_CREDENTIALS_PATH = join(resolvePluginData(), 'anthropic-oauth-credentials.json');
|
|
221
|
-
const CLAUDE_CODE_CREDENTIALS_PATH = join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'), '.credentials.json');
|
|
222
266
|
const CLAUDE_CODE_CLIENT_ID = process.env.ANTHROPIC_OAUTH_CLIENT_ID || '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
223
267
|
const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
|
|
224
268
|
const CLAUDE_AI_AUTHORIZE_URL = 'https://claude.com/cai/oauth/authorize';
|
|
@@ -235,15 +279,17 @@ const OAUTH_CALLBACK_HOST = 'localhost';
|
|
|
235
279
|
const OAUTH_CALLBACK_PORT = 54545;
|
|
236
280
|
const OAUTH_CALLBACK_PATH = '/callback';
|
|
237
281
|
const OAUTH_REDIRECT_URI = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
|
|
282
|
+
const OAUTH_MANUAL_REDIRECT_URI = process.env.ANTHROPIC_OAUTH_MANUAL_REDIRECT_URI || 'https://platform.claude.com/oauth/code/callback';
|
|
283
|
+
const OAUTH_SUCCESS_REDIRECT_URL = process.env.ANTHROPIC_OAUTH_SUCCESS_REDIRECT_URL || 'https://platform.claude.com/oauth/code/success?app=claude-code';
|
|
238
284
|
const OAUTH_LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
239
285
|
const OAUTH_TOKEN_TIMEOUT_MS = 30_000;
|
|
240
286
|
|
|
241
|
-
// Anthropic OAuth contract for first-party
|
|
287
|
+
// Anthropic OAuth contract for first-party OAuth clients.
|
|
242
288
|
// Opus/Sonnet requests are gated on a specific system-prompt prefix.
|
|
243
289
|
// Mixdog keeps that upstream client contract for OAuth routing. Haiku is not
|
|
244
290
|
// gated and ignores this prefix.
|
|
245
291
|
const CLAUDE_CODE_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
246
|
-
const OAUTH_BETA_HEADERS = 'oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,extended-cache-ttl-2025-04-11';
|
|
292
|
+
const OAUTH_BETA_HEADERS = 'oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,extended-cache-ttl-2025-04-11,advanced-tool-use-2025-11-20';
|
|
247
293
|
const DEFAULT_CLI_VERSION = '2.1.77';
|
|
248
294
|
|
|
249
295
|
function resolveCliVersion() {
|
|
@@ -252,23 +298,30 @@ function resolveCliVersion() {
|
|
|
252
298
|
}
|
|
253
299
|
|
|
254
300
|
function requiresSystemPrefix(model) {
|
|
255
|
-
// Opus / Sonnet require the
|
|
301
|
+
// Opus / Sonnet require the OAuth system prefix when authenticated
|
|
256
302
|
// via OAuth. Haiku does not.
|
|
257
303
|
return /^claude-(opus|sonnet)/i.test(String(model || ''));
|
|
258
304
|
}
|
|
259
305
|
|
|
260
306
|
// OAuth rate-limit pool routing is gated by the server inspecting the first
|
|
261
|
-
// system block. When it reads exactly
|
|
262
|
-
//
|
|
307
|
+
// system block. When it reads exactly the OAuth system prefix string it routes
|
|
308
|
+
// into the first-party OAuth pool; any other
|
|
263
309
|
// content (even the prefix concatenated with extra text in the same block)
|
|
264
310
|
// falls into the standard pool and Opus/Sonnet return 429. Splitting into
|
|
265
311
|
// two blocks — [prefix, rest] — keeps both routing and user instructions.
|
|
266
|
-
function buildSystemBlocks(
|
|
267
|
-
//
|
|
268
|
-
// content block with its own cache_control
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
312
|
+
function buildSystemBlocks(systemMsgs, model, systemTtl, tier3Ttl) {
|
|
313
|
+
// systemMsgs is an array of { content, cacheTier } — each non-empty element
|
|
314
|
+
// becomes its own Anthropic content block with its own cache_control
|
|
315
|
+
// breakpoint. Blocks tagged cacheTier:'tier3' (BP3 sessionMarker) take the
|
|
316
|
+
// tier3 TTL; every other block (BP1 baseRules / BP2 stableSystem) takes the
|
|
317
|
+
// system TTL. Invariant: callers must pass an array.
|
|
318
|
+
const items = Array.isArray(systemMsgs)
|
|
319
|
+
? systemMsgs
|
|
320
|
+
.map(m => ({
|
|
321
|
+
text: typeof m?.content === 'string' ? m.content.trim() : '',
|
|
322
|
+
tier: m?.cacheTier === 'tier3' ? 'tier3' : 'system',
|
|
323
|
+
}))
|
|
324
|
+
.filter(it => it.text)
|
|
272
325
|
: [];
|
|
273
326
|
const gated = requiresSystemPrefix(model);
|
|
274
327
|
|
|
@@ -276,22 +329,24 @@ function buildSystemBlocks(systemText, model, cacheControl, maxCacheBlocks = 2)
|
|
|
276
329
|
if (gated) {
|
|
277
330
|
blocks.push({ type: 'text', text: CLAUDE_CODE_SYSTEM_PREFIX });
|
|
278
331
|
}
|
|
279
|
-
for (let i = 0; i <
|
|
280
|
-
let body =
|
|
281
|
-
// Strip a duplicated
|
|
332
|
+
for (let i = 0; i < items.length; i++) {
|
|
333
|
+
let body = items[i].text;
|
|
334
|
+
// Strip a duplicated OAuth system prefix from the first block if present.
|
|
282
335
|
if (gated && i === 0 && body.startsWith(CLAUDE_CODE_SYSTEM_PREFIX)) {
|
|
283
336
|
body = body.slice(CLAUDE_CODE_SYSTEM_PREFIX.length).trim();
|
|
284
337
|
if (!body) continue;
|
|
285
338
|
}
|
|
286
|
-
blocks.push({ type: 'text', text: body });
|
|
339
|
+
blocks.push({ type: 'text', text: body, _tier: items[i].tier });
|
|
287
340
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
341
|
+
// Apply per-tier cache_control. BP1/BP2 -> systemTtl, BP3 -> tier3Ttl. The
|
|
342
|
+
// gating prefix block is never cached (Anthropic routes on its exact bytes).
|
|
343
|
+
// tier3Ttl === null leaves the 3rd block uncached (e.g. maintenance roles).
|
|
344
|
+
for (const b of blocks) {
|
|
345
|
+
const tier = b._tier;
|
|
346
|
+
delete b._tier;
|
|
347
|
+
if (b.text === CLAUDE_CODE_SYSTEM_PREFIX) continue;
|
|
348
|
+
const ttl = tier === 'tier3' ? tier3Ttl : systemTtl;
|
|
349
|
+
if (ttl) b.cache_control = ttl;
|
|
295
350
|
}
|
|
296
351
|
return blocks;
|
|
297
352
|
}
|
|
@@ -325,6 +380,18 @@ const EFFORT_BUDGET = {
|
|
|
325
380
|
max: 32768,
|
|
326
381
|
};
|
|
327
382
|
|
|
383
|
+
const MIN_THINKING_BUDGET = 1024;
|
|
384
|
+
const THINKING_OUTPUT_RESERVE = 1024;
|
|
385
|
+
|
|
386
|
+
function clampThinkingBudgetTokens(value, maxTokens) {
|
|
387
|
+
const desired = Math.floor(Number(value));
|
|
388
|
+
const max = Math.floor(Number(maxTokens));
|
|
389
|
+
if (!Number.isFinite(desired) || desired <= 0 || !Number.isFinite(max)) return null;
|
|
390
|
+
const ceiling = max - THINKING_OUTPUT_RESERVE;
|
|
391
|
+
if (ceiling < MIN_THINKING_BUDGET) return null;
|
|
392
|
+
return Math.max(MIN_THINKING_BUDGET, Math.min(desired, ceiling));
|
|
393
|
+
}
|
|
394
|
+
|
|
328
395
|
// Tracks which unknown effort labels we've already logged so a repeated
|
|
329
396
|
// session-level misconfig doesn't flood stderr with the same warning.
|
|
330
397
|
const _LOGGED_UNKNOWN_EFFORT = new Set();
|
|
@@ -345,7 +412,6 @@ function credentialCandidates() {
|
|
|
345
412
|
const paths = [];
|
|
346
413
|
_pushUnique(paths, process.env.ANTHROPIC_OAUTH_CREDENTIALS_PATH);
|
|
347
414
|
_pushUnique(paths, DEFAULT_CREDENTIALS_PATH);
|
|
348
|
-
_pushUnique(paths, CLAUDE_CODE_CREDENTIALS_PATH);
|
|
349
415
|
return paths;
|
|
350
416
|
}
|
|
351
417
|
|
|
@@ -385,21 +451,19 @@ function _loadCredentialsFile(path) {
|
|
|
385
451
|
}
|
|
386
452
|
}
|
|
387
453
|
|
|
388
|
-
// Cross-process safe
|
|
389
|
-
// from clobbering each other; atomic rename guarantees readers see
|
|
390
|
-
// the old or new file, never a half-written one. Used so refresh_token
|
|
391
|
-
// rotation propagates to other readers of the same credentials file
|
|
392
|
-
// leaving them stuck on the previous
|
|
393
|
-
// refresh_token. Mirrors openai-oauth.mjs:saveTokens.
|
|
454
|
+
// Cross-process safe credential save. Lockfile (O_EXCL) prevents two Mixdog
|
|
455
|
+
// refreshers from clobbering each other; atomic rename guarantees readers see
|
|
456
|
+
// either the old or new file, never a half-written one. Used so refresh_token
|
|
457
|
+
// rotation propagates to other Mixdog readers of the same credentials file
|
|
458
|
+
// instead of leaving them stuck on the previous refresh_token.
|
|
394
459
|
function _saveCredentialsFile(path, raw) {
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode: 0o600 });
|
|
460
|
+
// Secret file, not parent-dir ACL mutation. `secret: true` clamps the file
|
|
461
|
+
// itself on Windows; it deliberately leaves the data dir inheritance alone.
|
|
462
|
+
writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
|
|
399
463
|
}
|
|
400
464
|
|
|
401
|
-
// Cheap stat-only probe so ensureAuth can detect
|
|
402
|
-
//
|
|
465
|
+
// Cheap stat-only probe so ensureAuth can detect Mixdog-updated credentials
|
|
466
|
+
// without paying a full JSON read every call.
|
|
403
467
|
function _credentialsMaxMtime() {
|
|
404
468
|
let max = 0;
|
|
405
469
|
for (const p of credentialCandidates()) {
|
|
@@ -433,7 +497,7 @@ export function describeAnthropicOAuthCredentials() {
|
|
|
433
497
|
try {
|
|
434
498
|
const creds = loadCredentials();
|
|
435
499
|
if (!creds?.accessToken) {
|
|
436
|
-
return { authenticated: false, status: 'Not Set', detail:
|
|
500
|
+
return { authenticated: false, status: 'Not Set', detail: 'Mixdog OAuth credentials' };
|
|
437
501
|
}
|
|
438
502
|
const hasInferenceScope = Array.isArray(creds.scopes) && creds.scopes.includes('user:inference');
|
|
439
503
|
const hasRefresh = Boolean(creds.refreshToken);
|
|
@@ -495,7 +559,7 @@ function _scrubTokens(text) {
|
|
|
495
559
|
|
|
496
560
|
async function refreshOAuthCredentials(creds) {
|
|
497
561
|
if (!creds?.refreshToken) {
|
|
498
|
-
throw new Error('Anthropic OAuth refresh token not available.
|
|
562
|
+
throw new Error('Anthropic OAuth refresh token not available. Open /providers in mixdog to sign in again.');
|
|
499
563
|
}
|
|
500
564
|
|
|
501
565
|
const controller = new AbortController();
|
|
@@ -541,10 +605,10 @@ async function refreshOAuthCredentials(creds) {
|
|
|
541
605
|
scopes: Array.isArray(json?.scope) ? json.scope : creds.scopes,
|
|
542
606
|
subscriptionType: creds.subscriptionType,
|
|
543
607
|
};
|
|
544
|
-
// Persist rotated tokens back so any other reader of the same
|
|
545
|
-
// credentials file picks up the new refresh_token.
|
|
546
|
-
//
|
|
547
|
-
//
|
|
608
|
+
// Persist rotated tokens back so any other Mixdog reader of the same
|
|
609
|
+
// credentials file picks up the new refresh_token. Without this, a
|
|
610
|
+
// later process can replay an old single-use refresh token and loop on
|
|
611
|
+
// invalid_grant.
|
|
548
612
|
if (creds.path && existsSync(creds.path)) {
|
|
549
613
|
try {
|
|
550
614
|
const raw = JSON.parse(readFileSync(creds.path, 'utf-8'));
|
|
@@ -557,8 +621,8 @@ async function refreshOAuthCredentials(creds) {
|
|
|
557
621
|
};
|
|
558
622
|
_saveCredentialsFile(creds.path, raw);
|
|
559
623
|
} catch (err) {
|
|
560
|
-
process.stderr.write(`[anthropic-oauth] credential
|
|
561
|
-
throw new Error(`[oauth] credentials
|
|
624
|
+
process.stderr.write(`[anthropic-oauth] credential save failed: ${_scrubTokens(err?.message || String(err)).slice(0, 200)}\n`);
|
|
625
|
+
throw new Error(`[oauth] credentials save failed: ${err?.message ?? String(err)}`);
|
|
562
626
|
}
|
|
563
627
|
}
|
|
564
628
|
return refreshed;
|
|
@@ -642,9 +706,11 @@ function _sanitizeInputSchema(schema, toolName) {
|
|
|
642
706
|
if (addition) description = description ? `${description} ${addition}` : addition;
|
|
643
707
|
}
|
|
644
708
|
const mergedPropsCount = Object.keys(mergedProps).length;
|
|
645
|
-
process.
|
|
646
|
-
|
|
647
|
-
|
|
709
|
+
if (process.env.MIXDOG_DEBUG_SESSION_LOG) {
|
|
710
|
+
process.stderr.write(
|
|
711
|
+
`[anthropic-oauth-sanitizer] tool="${toolName ?? ''}" compound="${compoundKey}" branches=${Array.isArray(compound) ? compound.length : 0} mergedProps=${mergedPropsCount}\n`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
648
714
|
return {
|
|
649
715
|
type: 'object',
|
|
650
716
|
...(description ? { description } : {}),
|
|
@@ -653,11 +719,28 @@ function _sanitizeInputSchema(schema, toolName) {
|
|
|
653
719
|
}
|
|
654
720
|
|
|
655
721
|
function toAnthropicTools(tools) {
|
|
656
|
-
return tools.map(t =>
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
722
|
+
return tools.map(t => {
|
|
723
|
+
const out = {
|
|
724
|
+
name: t.name,
|
|
725
|
+
description: t.description,
|
|
726
|
+
input_schema: _sanitizeInputSchema(t.inputSchema, t.name),
|
|
727
|
+
};
|
|
728
|
+
if (t.deferLoading === true || t.defer_loading === true) out.defer_loading = true;
|
|
729
|
+
return out;
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
function nativeAnthropicTools(opts) {
|
|
733
|
+
return Array.isArray(opts?.nativeTools)
|
|
734
|
+
? opts.nativeTools.filter(t => t && typeof t === 'object')
|
|
735
|
+
: [];
|
|
736
|
+
}
|
|
737
|
+
function deferredAnthropicTools(activeTools, opts) {
|
|
738
|
+
if (opts?.session?.deferredNativeTools !== true) return [];
|
|
739
|
+
const active = new Set((activeTools || []).map((tool) => String(tool?.name || '').trim()).filter(Boolean));
|
|
740
|
+
const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
|
|
741
|
+
return catalog
|
|
742
|
+
.filter((tool) => tool?.name && !active.has(String(tool.name)))
|
|
743
|
+
.map((tool) => ({ ...tool, deferLoading: true }));
|
|
661
744
|
}
|
|
662
745
|
|
|
663
746
|
function toAnthropicMessages(messages) {
|
|
@@ -694,10 +777,15 @@ function toAnthropicMessages(messages) {
|
|
|
694
777
|
|
|
695
778
|
if (m.role === 'tool') {
|
|
696
779
|
const last = result[result.length - 1];
|
|
780
|
+
const refs = Array.isArray(m.nativeToolSearch?.toolReferences)
|
|
781
|
+
? m.nativeToolSearch.toolReferences.map((name) => String(name || '').trim()).filter(Boolean)
|
|
782
|
+
: [];
|
|
697
783
|
const block = {
|
|
698
784
|
type: 'tool_result',
|
|
699
785
|
tool_use_id: m.toolCallId || '',
|
|
700
|
-
content:
|
|
786
|
+
content: refs.length
|
|
787
|
+
? refs.map((name) => ({ type: 'tool_reference', tool_name: name }))
|
|
788
|
+
: normalizeContentForAnthropic(m.content),
|
|
701
789
|
};
|
|
702
790
|
if (last?.role === 'user' && Array.isArray(last.content)) {
|
|
703
791
|
last.content.push(block);
|
|
@@ -717,17 +805,15 @@ function toAnthropicMessages(messages) {
|
|
|
717
805
|
// sanitizeAnthropicContentPairs has already run (and must NOT run again
|
|
718
806
|
// after this), the blocks we mark here are exactly the blocks the provider
|
|
719
807
|
// sees, so the cache breakpoint is stable across turns.
|
|
720
|
-
// tier3: the user message whose first text block / string content
|
|
721
|
-
// startsWith '<system-reminder>' AND includes BP3_SENTINEL —
|
|
722
|
-
// mark its last content block with tier3Ttl.
|
|
723
808
|
// message-anchor: prefer a safe tool_result tail, then a previous real user
|
|
724
809
|
// text turn if another slot remains. Synthetic
|
|
725
810
|
// <system-reminder> messages and current pure-text prompts
|
|
726
811
|
// are excluded so first-turn prompts do not create a fresh
|
|
727
812
|
// BP4 write on every new session.
|
|
728
|
-
// messageTtl === null disables the tail
|
|
813
|
+
// messageTtl === null disables the tail. BP3 (tier3) now rides a system block,
|
|
814
|
+
// so it is no longer marked here.
|
|
729
815
|
// ANTHROPIC_MSG_SLOTS=0 is honoured upstream by passing messageTtl = null.
|
|
730
|
-
function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_VOLATILE, messageSlots = 1
|
|
816
|
+
function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_VOLATILE, messageSlots = 1 } = {}) {
|
|
731
817
|
if (!Array.isArray(sanitizedMessages) || sanitizedMessages.length === 0) {
|
|
732
818
|
return sanitizedMessages;
|
|
733
819
|
}
|
|
@@ -741,29 +827,20 @@ function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_
|
|
|
741
827
|
return '';
|
|
742
828
|
};
|
|
743
829
|
const isSystemReminder = (content) => firstText(content).startsWith('<system-reminder>');
|
|
744
|
-
const isTier3SystemReminder = (content) => {
|
|
745
|
-
const text = firstText(content);
|
|
746
|
-
return text.startsWith('<system-reminder>') && text.includes(BP3_SENTINEL);
|
|
747
|
-
};
|
|
748
830
|
|
|
749
831
|
const markLast = (msg, ttl) => {
|
|
750
832
|
if (!msg) return;
|
|
751
833
|
msg.content = appendCacheControl(msg.content, ttl);
|
|
752
834
|
};
|
|
753
835
|
const ttlRank = (ttl) => ttl?.ttl === '1h' ? 2 : 1;
|
|
754
|
-
const canMarkMessageIdx = (idx
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
// protects raw/default callers.
|
|
836
|
+
const canMarkMessageIdx = (idx) => {
|
|
837
|
+
// System-reminder messages (volatileTail / roleSpecific BP4) vary
|
|
838
|
+
// per-call, so never pin them with a 1h marker. The 1h system blocks
|
|
839
|
+
// (BP1/BP2/BP3) already satisfy Anthropic's "1h before 5m" ordering.
|
|
759
840
|
if (idx < 0) return false;
|
|
760
841
|
const msg = sanitizedMessages[idx];
|
|
761
842
|
if (ttlRank(messageTtl) > ttlRank(CACHE_TTL_VOLATILE)
|
|
762
|
-
&& isSystemReminder(msg?.content)
|
|
763
|
-
&& !isTier3SystemReminder(msg?.content)) {
|
|
764
|
-
return false;
|
|
765
|
-
}
|
|
766
|
-
if (tier3Idx >= 0 && idx < tier3Idx && ttlRank(messageTtl) < ttlRank(tier3Ttl)) {
|
|
843
|
+
&& isSystemReminder(msg?.content)) {
|
|
767
844
|
return false;
|
|
768
845
|
}
|
|
769
846
|
return true;
|
|
@@ -798,29 +875,16 @@ function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_
|
|
|
798
875
|
return -1;
|
|
799
876
|
};
|
|
800
877
|
|
|
801
|
-
// tier3 — locate the sentinel-tagged system-reminder user message.
|
|
802
|
-
let tier3MsgIdx = -1;
|
|
803
|
-
if (tier3Ttl !== null) {
|
|
804
|
-
for (let i = 0; i < sanitizedMessages.length; i++) {
|
|
805
|
-
const m = sanitizedMessages[i];
|
|
806
|
-
if (m?.role === 'user' && isTier3SystemReminder(m.content)) {
|
|
807
|
-
tier3MsgIdx = i;
|
|
808
|
-
break;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
if (tier3MsgIdx >= 0) markLast(sanitizedMessages[tier3MsgIdx], tier3Ttl);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
878
|
if (messageTtl !== null) {
|
|
815
879
|
const slots = Math.max(0, Math.min(4, Number(messageSlots) || 0));
|
|
816
|
-
const marked = new Set(
|
|
880
|
+
const marked = new Set();
|
|
817
881
|
const candidates = [latestToolResultTailIdx(), previousUserTextAnchorIdx()];
|
|
818
882
|
for (const idx of candidates) {
|
|
819
883
|
if (slots <= 0) break;
|
|
820
|
-
if (idx < 0 || marked.has(idx) || !canMarkMessageIdx(idx
|
|
884
|
+
if (idx < 0 || marked.has(idx) || !canMarkMessageIdx(idx)) continue;
|
|
821
885
|
markLast(sanitizedMessages[idx], messageTtl);
|
|
822
886
|
marked.add(idx);
|
|
823
|
-
if (marked.size
|
|
887
|
+
if (marked.size >= slots) break;
|
|
824
888
|
}
|
|
825
889
|
}
|
|
826
890
|
|
|
@@ -832,13 +896,19 @@ function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_
|
|
|
832
896
|
function _captureMidstreamAbort(state, reason) {
|
|
833
897
|
if (!state) return;
|
|
834
898
|
const reasonName = reason?.name || '';
|
|
835
|
-
if (reasonName === '
|
|
899
|
+
if (reasonName === 'AgentStallAbortError' || reasonName === 'StreamStalledAbortError') {
|
|
836
900
|
state.watchdogAbort = reasonName;
|
|
837
901
|
} else {
|
|
838
902
|
state.userAbort = true;
|
|
839
903
|
}
|
|
840
904
|
}
|
|
841
905
|
|
|
906
|
+
// Abort-aware mid-stream backoff sleep → shared sleepWithAbort
|
|
907
|
+
// (retry-classifier.mjs). abortMessage preserves the prior fallback text.
|
|
908
|
+
function _midstreamSleepWithAbort(ms, signal) {
|
|
909
|
+
return sleepWithAbort(ms, signal, undefined, 'Anthropic OAuth mid-stream retry backoff aborted');
|
|
910
|
+
}
|
|
911
|
+
|
|
842
912
|
function _statusForAnthropicSseError(type, message) {
|
|
843
913
|
const kind = String(type || '').toLowerCase();
|
|
844
914
|
const text = String(message || '').toLowerCase();
|
|
@@ -894,7 +964,7 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
894
964
|
const resetIdleTimer = () => {
|
|
895
965
|
// OFF by default. When disabled the
|
|
896
966
|
// idle timer never arms, so the stream is never killed on inactivity;
|
|
897
|
-
// the
|
|
967
|
+
// the agent stall watchdog (600s) remains the dead-stream backstop.
|
|
898
968
|
if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED) return;
|
|
899
969
|
if (idleTimer) clearTimeout(idleTimer);
|
|
900
970
|
idleTimer = setTimeout(() => {
|
|
@@ -975,7 +1045,7 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
975
1045
|
if (line.startsWith(':')) {
|
|
976
1046
|
// SSE comment frame (Anthropic `:ping` keepalive). The HTML Standard SSE
|
|
977
1047
|
// spec says comments are silently ignored, but we surface them here so
|
|
978
|
-
// the
|
|
1048
|
+
// the agent stall watchdog sees the stream is still alive during Opus
|
|
979
1049
|
// extended-thinking pauses. No content is emitted — this only refreshes
|
|
980
1050
|
// the runtime's lastStreamDeltaAt timestamp.
|
|
981
1051
|
try { onStreamDelta?.(); } catch {}
|
|
@@ -1062,15 +1132,15 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1062
1132
|
// whole tool_call — the loop never saw it and
|
|
1063
1133
|
// the assistant turn ended with an unmatched
|
|
1064
1134
|
// tool_use id. Wrap the parse so a malformed
|
|
1065
|
-
// input still produces a tool_call (with
|
|
1066
|
-
//
|
|
1067
|
-
// silent drop.
|
|
1135
|
+
// input still produces a tool_call (with an
|
|
1136
|
+
// invalid-args marker and a logged error) instead
|
|
1137
|
+
// of a silent drop or accidental `{}` dispatch.
|
|
1068
1138
|
let parsedArgs = {};
|
|
1069
1139
|
if (pending.inputJson) {
|
|
1070
1140
|
try { parsedArgs = JSON.parse(pending.inputJson); }
|
|
1071
1141
|
catch (parseErr) {
|
|
1072
1142
|
process.stderr.write(`[anthropic-oauth] tool args JSON.parse failed (id=${pending.id}, name=${pending.name}): ${parseErr?.message || parseErr}\n`);
|
|
1073
|
-
parsedArgs =
|
|
1143
|
+
parsedArgs = makeInvalidToolArgsMarker(pending.inputJson, parseErr instanceof Error ? parseErr.message : String(parseErr));
|
|
1074
1144
|
}
|
|
1075
1145
|
}
|
|
1076
1146
|
// Tool arguments must be a plain object. Anthropic's
|
|
@@ -1187,35 +1257,14 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1187
1257
|
* That keeps recovery limited to transport/stream stalls without risking
|
|
1188
1258
|
* duplicate eager tool execution.
|
|
1189
1259
|
*/
|
|
1260
|
+
// Thin wrapper: the SSE mid-stream decision tree now lives in the shared
|
|
1261
|
+
// classifyMidstreamError (retry-classifier.mjs, policy.mode='sse'). Kept as a
|
|
1262
|
+
// named export so internal call sites AND anthropic.mjs (which imports this
|
|
1263
|
+
// symbol) keep resolving it. Behavior is byte-identical — the shared function
|
|
1264
|
+
// is the relocated original, gated by SSE_MIDSTREAM_POLICY (defaultRetries=3,
|
|
1265
|
+
// perClassifierGate:false).
|
|
1190
1266
|
export function _classifyMidstreamError(err, state) {
|
|
1191
|
-
|
|
1192
|
-
if ((state.attemptIndex | 0) >= 1) return null;
|
|
1193
|
-
if (state.sawCompleted) return null;
|
|
1194
|
-
if (!state.sawMessageStart) return null;
|
|
1195
|
-
if (state.userAbort) return null;
|
|
1196
|
-
if (state.emittedToolCall) return null;
|
|
1197
|
-
|
|
1198
|
-
if (!err) return null;
|
|
1199
|
-
const status = Number(err?.httpStatus || 0);
|
|
1200
|
-
if (status === 401 || status === 403 || status === 429) return null;
|
|
1201
|
-
|
|
1202
|
-
const name = err?.name || '';
|
|
1203
|
-
if (name === 'BridgeStallAbortError') return 'bridge_stall';
|
|
1204
|
-
if (name === 'StreamStalledAbortError') return 'stream_stalled';
|
|
1205
|
-
if (state.watchdogAbort === 'BridgeStallAbortError') return 'bridge_stall';
|
|
1206
|
-
if (state.watchdogAbort === 'StreamStalledAbortError') return 'stream_stalled';
|
|
1207
|
-
|
|
1208
|
-
const code = err?.code || err?.cause?.code || '';
|
|
1209
|
-
if (code === 'ECONNRESET') return 'reset';
|
|
1210
|
-
if (code === 'ETIMEDOUT' || code === 'ESOCKETTIMEDOUT') return 'timeout';
|
|
1211
|
-
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'EAI_NODATA') return 'dns';
|
|
1212
|
-
|
|
1213
|
-
const msg = String(err?.message || '').toLowerCase();
|
|
1214
|
-
if (msg.includes('stream timed out after') && msg.includes('of inactivity')) return 'sse_idle_timeout';
|
|
1215
|
-
if (msg.includes('body stream') && msg.includes('terminated')) return 'stream_terminated';
|
|
1216
|
-
if (msg.includes('fetch failed')) return 'fetch_failed';
|
|
1217
|
-
|
|
1218
|
-
return null;
|
|
1267
|
+
return classifyMidstreamError(err, state, SSE_MIDSTREAM_POLICY);
|
|
1219
1268
|
}
|
|
1220
1269
|
|
|
1221
1270
|
// --- Build request body ---
|
|
@@ -1232,69 +1281,51 @@ function resolveCacheTtls(opts) {
|
|
|
1232
1281
|
return fallback;
|
|
1233
1282
|
};
|
|
1234
1283
|
// BP budget (4 total):
|
|
1235
|
-
// BP1 baseRules — 1h (shared
|
|
1236
|
-
// BP2
|
|
1237
|
-
// BP3 tier3 — 1h (sessionMarker:
|
|
1284
|
+
// BP1 baseRules — 1h (shared tool policy + compact skill manifest)
|
|
1285
|
+
// BP2 stableSystem — 1h (role/system rules)
|
|
1286
|
+
// BP3 tier3 — 1h (sessionMarker: stable memory/meta body)
|
|
1238
1287
|
// BP4 messages — 1h sliding tail (tool_result cache across iter)
|
|
1239
1288
|
// tools BP is dropped — system BP covers the tools prefix via
|
|
1240
1289
|
// Anthropic's prompt cache prefix semantics (order: tools → system
|
|
1241
1290
|
// → messages).
|
|
1242
1291
|
// tier3 defaults to 1h (stable) — sessionMarker content is stable per
|
|
1243
|
-
//
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
// though their message layout supported it.
|
|
1292
|
+
// memory/meta tuple and the BP slot is only spent when a 3rd
|
|
1293
|
+
// (cacheTier:'tier3') system block is actually present, so this default is
|
|
1294
|
+
// free for sessions that don't carry one. Previously null here meant any
|
|
1295
|
+
// caller that skipped agent runtime resolve (CLI, raw agent spawn)
|
|
1296
|
+
// silently lost the tier3 cache layer even though it supported one.
|
|
1249
1297
|
return {
|
|
1250
|
-
tools: pick('tools',
|
|
1298
|
+
tools: pick('tools', null),
|
|
1251
1299
|
system: pick('system', CACHE_TTL_STABLE),
|
|
1252
1300
|
tier3: pick('tier3', CACHE_TTL_STABLE),
|
|
1253
1301
|
messages: pick('messages', CACHE_TTL_STABLE),
|
|
1254
1302
|
};
|
|
1255
1303
|
}
|
|
1256
1304
|
|
|
1257
|
-
//
|
|
1258
|
-
//
|
|
1259
|
-
//
|
|
1260
|
-
//
|
|
1261
|
-
// volatileTail (role/permission/taskBrief/memoryRecap) is also wrapped in
|
|
1262
|
-
// `<system-reminder>` but varies per-call, so a plain prefix match would
|
|
1263
|
-
// pin per-call data to the 1h BP3 slot and explode the cache.
|
|
1264
|
-
const BP3_SENTINEL = '<!-- bp3-sentinel -->';
|
|
1265
|
-
function findTier3Index(chatMsgs) {
|
|
1266
|
-
for (let i = 0; i < chatMsgs.length; i++) {
|
|
1267
|
-
const m = chatMsgs[i];
|
|
1268
|
-
if (m?.role === 'user' && typeof m.content === 'string'
|
|
1269
|
-
&& m.content.startsWith('<system-reminder>')
|
|
1270
|
-
&& m.content.includes(BP3_SENTINEL)) {
|
|
1271
|
-
return i;
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
return -1;
|
|
1275
|
-
}
|
|
1305
|
+
// BP3 (tier3) is injected by session/manager as its own `system` role block —
|
|
1306
|
+
// the 3rd system block, tagged `cacheTier:'tier3'`. buildSystemBlocks applies
|
|
1307
|
+
// the tier3 1h cache_control to that block; BP1/BP2 take the system TTL. No
|
|
1308
|
+
// `<system-reminder>` user message / sentinel scan is involved anymore.
|
|
1276
1309
|
|
|
1277
1310
|
function buildRequestBody(messages, model, tools, sendOpts) {
|
|
1278
1311
|
const systemMsgs = messages.filter(m => m.role === 'system');
|
|
1279
1312
|
const chatMsgs = messages.filter(m => m.role !== 'system');
|
|
1280
|
-
// Pass each system message text as its own entry so the Anthropic body
|
|
1281
|
-
// gets N separate content blocks — each can have its own BP
|
|
1282
|
-
// independent of the others.
|
|
1283
|
-
const systemTexts = systemMsgs.map(m => m.content);
|
|
1284
1313
|
const maxTokens = resolveMaxTokens(model);
|
|
1285
1314
|
const opts = sendOpts || {};
|
|
1286
1315
|
const ttls = resolveCacheTtls(opts);
|
|
1287
|
-
|
|
1316
|
+
// Each system message becomes its own Anthropic content block with its own
|
|
1317
|
+
// breakpoint: BP1 baseRules + BP2 stableSystem at ttls.system, BP3
|
|
1318
|
+
// sessionMarker (cacheTier:'tier3') at ttls.tier3.
|
|
1319
|
+
const systemBlocks = buildSystemBlocks(systemMsgs, model, ttls?.system, ttls?.tier3);
|
|
1288
1320
|
|
|
1289
1321
|
// 4-BP budget layout. tools BP is dropped — system BP covers the
|
|
1290
1322
|
// tools prefix via Anthropic's prompt cache prefix semantics
|
|
1291
|
-
// (order: tools → system → messages). That frees slots for
|
|
1292
|
-
//
|
|
1293
|
-
|
|
1323
|
+
// (order: tools → system → messages). That frees slots for the
|
|
1324
|
+
// messages-tail. The system blocks now hold BP1/BP2/BP3 (tier3), so the
|
|
1325
|
+
// tier3 breakpoint is accounted for inside systemBpUsed.
|
|
1326
|
+
const systemBpUsed = systemBlocks.filter(b => b.cache_control).length;
|
|
1294
1327
|
const toolsBpUsed = 0;
|
|
1295
|
-
const
|
|
1296
|
-
const tier3BpUsed = tier3Idx >= 0 ? 1 : 0;
|
|
1297
|
-
const usedSlots = toolsBpUsed + systemBpUsed + tier3BpUsed;
|
|
1328
|
+
const usedSlots = toolsBpUsed + systemBpUsed;
|
|
1298
1329
|
// Env override for BP strategy. ANTHROPIC_MSG_SLOTS=0 disables message
|
|
1299
1330
|
// caching entirely. Any value >=1 first marks the previous user text turn
|
|
1300
1331
|
// so consecutive requests share a breakpoint; a second free slot marks the
|
|
@@ -1310,10 +1341,9 @@ function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
1310
1341
|
// marked block. NEVER sanitize again after this (see send path).
|
|
1311
1342
|
// msgSlots === 0 (ANTHROPIC_MSG_SLOTS=0, or no free slot) → tail disabled.
|
|
1312
1343
|
const tailTtl = msgSlots > 0 ? ttls.messages : null;
|
|
1313
|
-
const tier3Ttl = tier3Idx >= 0 ? ttls.tier3 : null;
|
|
1314
1344
|
const anthropicMessages = applyAnthropicCacheMarkers(
|
|
1315
1345
|
toAnthropicMessages(chatMsgs),
|
|
1316
|
-
{ messageTtl: tailTtl, messageSlots: msgSlots
|
|
1346
|
+
{ messageTtl: tailTtl, messageSlots: msgSlots },
|
|
1317
1347
|
);
|
|
1318
1348
|
|
|
1319
1349
|
const body = {
|
|
@@ -1325,20 +1355,24 @@ function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
1325
1355
|
|
|
1326
1356
|
if (systemBlocks.length) body.system = systemBlocks;
|
|
1327
1357
|
|
|
1328
|
-
|
|
1358
|
+
const nativeTools = nativeAnthropicTools(opts);
|
|
1359
|
+
const deferredTools = deferredAnthropicTools(tools || [], opts);
|
|
1360
|
+
if (tools?.length || nativeTools.length || deferredTools.length) {
|
|
1329
1361
|
// No cache_control on tools — the systemBase BP already covers the
|
|
1330
1362
|
// tools prefix via Anthropic's prompt cache prefix semantics (order:
|
|
1331
1363
|
// tools → system → messages). Placing a separate BP here would waste
|
|
1332
1364
|
// a slot that's better spent on messages tail.
|
|
1333
|
-
body.tools = toAnthropicTools(tools);
|
|
1365
|
+
body.tools = [...nativeTools, ...toAnthropicTools([...(tools || []), ...deferredTools])];
|
|
1334
1366
|
}
|
|
1335
1367
|
|
|
1336
1368
|
const thinkingBudgetTokens = Number(opts.thinkingBudgetTokens);
|
|
1337
1369
|
if (Number.isFinite(thinkingBudgetTokens) && thinkingBudgetTokens > 0) {
|
|
1338
|
-
|
|
1370
|
+
const budgetTokens = clampThinkingBudgetTokens(thinkingBudgetTokens, maxTokens);
|
|
1371
|
+
if (budgetTokens) body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
|
|
1339
1372
|
} else if (opts.effort) {
|
|
1340
1373
|
if (EFFORT_BUDGET[opts.effort]) {
|
|
1341
|
-
|
|
1374
|
+
const budgetTokens = clampThinkingBudgetTokens(EFFORT_BUDGET[opts.effort], maxTokens);
|
|
1375
|
+
if (budgetTokens) body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
|
|
1342
1376
|
} else if (!_LOGGED_UNKNOWN_EFFORT.has(opts.effort)) {
|
|
1343
1377
|
_LOGGED_UNKNOWN_EFFORT.add(opts.effort);
|
|
1344
1378
|
try {
|
|
@@ -1382,12 +1416,12 @@ export class AnthropicOAuthProvider {
|
|
|
1382
1416
|
this.credentials = loadCredentials();
|
|
1383
1417
|
}
|
|
1384
1418
|
if (!this.credentials) {
|
|
1385
|
-
throw new Error('Anthropic OAuth credentials not found.
|
|
1419
|
+
throw new Error('Anthropic OAuth credentials not found. Open /providers in mixdog to sign in.');
|
|
1386
1420
|
}
|
|
1387
1421
|
|
|
1388
|
-
// Pick up
|
|
1389
|
-
// rewritten — without this, a fresh
|
|
1390
|
-
// the in-memory token's expiry skew triggers a refresh.
|
|
1422
|
+
// Pick up Mixdog-updated tokens the moment the credentials file is
|
|
1423
|
+
// rewritten — without this, a fresh /auth login in another process is
|
|
1424
|
+
// ignored until the in-memory token's expiry skew triggers a refresh.
|
|
1391
1425
|
const diskMtime = _credentialsMaxMtime();
|
|
1392
1426
|
if (diskMtime > 0 && diskMtime > (this.credentials.mtimeMs || 0)) {
|
|
1393
1427
|
const fresh = loadCredentials();
|
|
@@ -1439,7 +1473,7 @@ export class AnthropicOAuthProvider {
|
|
|
1439
1473
|
process.stderr.write(`[anthropic-oauth] WARNING: token expiring but no refresh token; using current token until expiry\n`);
|
|
1440
1474
|
return latest;
|
|
1441
1475
|
}
|
|
1442
|
-
throw new Error('Anthropic OAuth refresh token not available.
|
|
1476
|
+
throw new Error('Anthropic OAuth refresh token not available. Open /providers in mixdog to sign in again.');
|
|
1443
1477
|
}
|
|
1444
1478
|
|
|
1445
1479
|
try {
|
|
@@ -1466,6 +1500,11 @@ export class AnthropicOAuthProvider {
|
|
|
1466
1500
|
}
|
|
1467
1501
|
|
|
1468
1502
|
async send(messages, model, tools, sendOpts) {
|
|
1503
|
+
// Re-warm the kept-alive socket before the turn. preconnect() is a
|
|
1504
|
+
// best-effort no-op while a socket is still hot (TTL gate), but after an
|
|
1505
|
+
// idle gap longer than the keep-alive window it re-opens one in parallel
|
|
1506
|
+
// with auth/body build so the POST below skips the cold TLS handshake.
|
|
1507
|
+
preconnect('https://api.anthropic.com');
|
|
1469
1508
|
// Defense-in-depth: enforce tool_use / tool_result pairing before
|
|
1470
1509
|
// the Anthropic API call. The trim.mjs sanitize pass is normally
|
|
1471
1510
|
// invoked by the budget trimmer in loop.mjs, but dispatches under
|
|
@@ -1502,7 +1541,7 @@ export class AnthropicOAuthProvider {
|
|
|
1502
1541
|
// (PROVIDER_HTTP_RESPONSE_TIMEOUT_MS) for a socket that never sends a
|
|
1503
1542
|
// first byte (truly wedged),
|
|
1504
1543
|
// (b) externalSignal (client disconnect / replaced-by-newer-request), and
|
|
1505
|
-
// (c) the
|
|
1544
|
+
// (c) the agent stall watchdog (STALL_ABORT_S, 600s, progress-based) plus
|
|
1506
1545
|
// the optional SSE idle watchdog for a stream that goes dead mid-flight.
|
|
1507
1546
|
// totalSignal is therefore a pure pass-through of externalSignal with no timer.
|
|
1508
1547
|
const totalTimeout = createPassthroughSignal(externalSignal);
|
|
@@ -1558,6 +1597,7 @@ export class AnthropicOAuthProvider {
|
|
|
1558
1597
|
'anthropic-beta': buildAnthropicBetaHeaders({
|
|
1559
1598
|
base: OAUTH_BETA_HEADERS,
|
|
1560
1599
|
fastMode: this.fastModeBetaHeaderLatched,
|
|
1600
|
+
toolSearch: true,
|
|
1561
1601
|
}),
|
|
1562
1602
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
1563
1603
|
'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
|
|
@@ -1569,7 +1609,7 @@ export class AnthropicOAuthProvider {
|
|
|
1569
1609
|
dispatcher: getLlmDispatcher(),
|
|
1570
1610
|
});
|
|
1571
1611
|
|
|
1572
|
-
|
|
1612
|
+
traceAgentFetch({
|
|
1573
1613
|
sessionId,
|
|
1574
1614
|
headersMs: Date.now() - fetchStartedAt,
|
|
1575
1615
|
httpStatus: response.status,
|
|
@@ -1611,6 +1651,12 @@ export class AnthropicOAuthProvider {
|
|
|
1611
1651
|
const status = Number(result?.response?.status || 0);
|
|
1612
1652
|
const transientStatus = classifyError({ httpStatus: status }) === 'transient';
|
|
1613
1653
|
if (transientStatus || status === 429) {
|
|
1654
|
+
if (status === 429) {
|
|
1655
|
+
const quotaText = await result.response.text().catch(() => '');
|
|
1656
|
+
cleanupCancelHandler(result.cancelHandler);
|
|
1657
|
+
try { result.controller?.abort?.(); } catch {}
|
|
1658
|
+
throw anthropicQuotaError(status, result?.response?.headers, this.scrubTokens(quotaText));
|
|
1659
|
+
}
|
|
1614
1660
|
const err = new Error(`Anthropic OAuth API ${status}`);
|
|
1615
1661
|
err.httpStatus = status;
|
|
1616
1662
|
err.status = status;
|
|
@@ -1642,9 +1688,9 @@ export class AnthropicOAuthProvider {
|
|
|
1642
1688
|
} catch {}
|
|
1643
1689
|
},
|
|
1644
1690
|
});
|
|
1645
|
-
//
|
|
1646
|
-
//
|
|
1647
|
-
const MAX_MIDSTREAM_RETRIES =
|
|
1691
|
+
// Bounded mid-stream retries for transient stream loss; jittered backoff
|
|
1692
|
+
// between attempts (see catch branches).
|
|
1693
|
+
const MAX_MIDSTREAM_RETRIES = ANTHROPIC_MAX_MIDSTREAM_RETRIES;
|
|
1648
1694
|
let firstAttemptError = null;
|
|
1649
1695
|
let firstAttemptClassifier = null;
|
|
1650
1696
|
|
|
@@ -1669,6 +1715,10 @@ export class AnthropicOAuthProvider {
|
|
|
1669
1715
|
const safeText = this.scrubTokens(text).slice(0, 200);
|
|
1670
1716
|
process.stderr.write(`[anthropic-oauth] API error ${response.status}: ${safeText}\n`);
|
|
1671
1717
|
|
|
1718
|
+
if (response.status === 429) {
|
|
1719
|
+
throw anthropicQuotaError(response.status, response.headers, safeText);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1672
1722
|
// Phase I: on unknown/404 model errors, force a catalog refresh and
|
|
1673
1723
|
// retry once. Protects against a silently-rotated model id.
|
|
1674
1724
|
const isUnknownModel = response.status === 404
|
|
@@ -1717,7 +1767,7 @@ export class AnthropicOAuthProvider {
|
|
|
1717
1767
|
|
|
1718
1768
|
const ttftMs = midState.ttftAt ? midState.ttftAt - sseStartedAt : null;
|
|
1719
1769
|
const liveModel = result.model || useModel;
|
|
1720
|
-
|
|
1770
|
+
traceAgentSse({
|
|
1721
1771
|
sessionId,
|
|
1722
1772
|
sseParseMs: Date.now() - sseStartedAt,
|
|
1723
1773
|
ttftMs,
|
|
@@ -1726,7 +1776,7 @@ export class AnthropicOAuthProvider {
|
|
|
1726
1776
|
transport: 'sse',
|
|
1727
1777
|
});
|
|
1728
1778
|
|
|
1729
|
-
|
|
1779
|
+
traceAgentUsage({
|
|
1730
1780
|
sessionId,
|
|
1731
1781
|
iteration,
|
|
1732
1782
|
inputTokens: result.usage?.inputTokens || 0,
|
|
@@ -1799,6 +1849,7 @@ export class AnthropicOAuthProvider {
|
|
|
1799
1849
|
firstAttemptClassifier = 'empty_stream';
|
|
1800
1850
|
try { controller?.abort?.(err); } catch { /* best-effort teardown */ }
|
|
1801
1851
|
try { process.stderr.write(`[anthropic-oauth] empty stream (no message_start) — retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES}\n`); } catch {}
|
|
1852
|
+
await _midstreamSleepWithAbort(midstreamBackoffFor(attemptIndex + 1), totalSignal);
|
|
1802
1853
|
continue;
|
|
1803
1854
|
}
|
|
1804
1855
|
if (classifyError(err) === 'transient'
|
|
@@ -1811,6 +1862,7 @@ export class AnthropicOAuthProvider {
|
|
|
1811
1862
|
try {
|
|
1812
1863
|
process.stderr.write(`[anthropic-oauth] transient SSE error — retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES} (${err?.providerErrorType || err?.message || 'unknown'})\n`);
|
|
1813
1864
|
} catch {}
|
|
1865
|
+
await _midstreamSleepWithAbort(midstreamBackoffFor(attemptIndex + 1), totalSignal);
|
|
1814
1866
|
continue;
|
|
1815
1867
|
}
|
|
1816
1868
|
// Truncated stream (message_start without message_stop): the
|
|
@@ -1832,6 +1884,7 @@ export class AnthropicOAuthProvider {
|
|
|
1832
1884
|
firstAttemptClassifier = 'truncated_stream';
|
|
1833
1885
|
try { controller?.abort?.(err); } catch { /* best-effort teardown */ }
|
|
1834
1886
|
try { process.stderr.write(`[anthropic-oauth] truncated stream — retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES}\n`); } catch {}
|
|
1887
|
+
await _midstreamSleepWithAbort(midstreamBackoffFor(attemptIndex + 1), totalSignal);
|
|
1835
1888
|
continue;
|
|
1836
1889
|
}
|
|
1837
1890
|
const classifier = _classifyMidstreamError(err, midState);
|
|
@@ -1845,6 +1898,7 @@ export class AnthropicOAuthProvider {
|
|
|
1845
1898
|
try {
|
|
1846
1899
|
process.stderr.write(`[anthropic-oauth] mid-stream recovered: retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES} (cause: ${classifier})\n`);
|
|
1847
1900
|
} catch {}
|
|
1901
|
+
await _midstreamSleepWithAbort(midstreamBackoffFor(attemptIndex + 1), totalSignal);
|
|
1848
1902
|
continue;
|
|
1849
1903
|
}
|
|
1850
1904
|
if (attemptIndex > 0 && firstAttemptError) {
|
|
@@ -1982,31 +2036,118 @@ function _oauthParseScopeField(scope) {
|
|
|
1982
2036
|
return String(scope || '').split(' ').filter(Boolean);
|
|
1983
2037
|
}
|
|
1984
2038
|
|
|
1985
|
-
|
|
2039
|
+
function _parseOAuthCodeInput(input) {
|
|
2040
|
+
const value = String(input || '').trim();
|
|
2041
|
+
if (!value) return { code: '', state: '' };
|
|
2042
|
+
try {
|
|
2043
|
+
const url = new URL(value);
|
|
2044
|
+
const code = url.searchParams.get('code') || '';
|
|
2045
|
+
const state = url.searchParams.get('state') || '';
|
|
2046
|
+
if (code || state) return { code, state, redirectUri: `${url.origin}${url.pathname}` };
|
|
2047
|
+
} catch { /* not a URL */ }
|
|
2048
|
+
if (value.includes('#')) {
|
|
2049
|
+
const [code, state] = value.split('#', 2);
|
|
2050
|
+
return { code: String(code || '').trim(), state: String(state || '').trim() };
|
|
2051
|
+
}
|
|
2052
|
+
if (value.includes('code=')) {
|
|
2053
|
+
const params = new URLSearchParams(value.startsWith('?') ? value.slice(1) : value);
|
|
2054
|
+
return { code: params.get('code') || '', state: params.get('state') || '' };
|
|
2055
|
+
}
|
|
2056
|
+
return { code: value, state: '' };
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
async function exchangeAuthorizationCode({ pkce, code, state, redirectUri }) {
|
|
2060
|
+
const cleanCode = String(code || '').trim();
|
|
2061
|
+
if (!cleanCode) throw new Error('[anthropic-oauth] authorization code is required');
|
|
2062
|
+
const tokenRes = await fetch(TOKEN_URL, {
|
|
2063
|
+
method: 'POST',
|
|
2064
|
+
headers: {
|
|
2065
|
+
'Content-Type': 'application/json',
|
|
2066
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
2067
|
+
'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
|
|
2068
|
+
},
|
|
2069
|
+
body: JSON.stringify({
|
|
2070
|
+
grant_type: 'authorization_code',
|
|
2071
|
+
code: cleanCode,
|
|
2072
|
+
redirect_uri: redirectUri,
|
|
2073
|
+
client_id: CLAUDE_CODE_CLIENT_ID,
|
|
2074
|
+
code_verifier: pkce.verifier,
|
|
2075
|
+
state,
|
|
2076
|
+
}),
|
|
2077
|
+
redirect: 'error',
|
|
2078
|
+
signal: AbortSignal.timeout(OAUTH_TOKEN_TIMEOUT_MS),
|
|
2079
|
+
dispatcher: getLlmDispatcher(),
|
|
2080
|
+
});
|
|
2081
|
+
if (!tokenRes.ok) {
|
|
2082
|
+
const text = await tokenRes.text().catch(() => '');
|
|
2083
|
+
throw new Error(`[anthropic-oauth] token exchange ${tokenRes.status}: ${_scrubTokens(text).slice(0, 500)}`);
|
|
2084
|
+
}
|
|
2085
|
+
const json = await tokenRes.json();
|
|
2086
|
+
const accessToken = json?.access_token || json?.accessToken;
|
|
2087
|
+
const refreshToken = json?.refresh_token || json?.refreshToken;
|
|
2088
|
+
if (!accessToken || !refreshToken) {
|
|
2089
|
+
throw new Error('[anthropic-oauth] token exchange response missing access_token or refresh_token');
|
|
2090
|
+
}
|
|
2091
|
+
const expiresAt = _normalizeExpiresAt(json?.expires_at ?? json?.expiresAt)
|
|
2092
|
+
|| (typeof json?.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0);
|
|
2093
|
+
const scopes = _oauthParseScopeField(json?.scope);
|
|
2094
|
+
const credPath = _oauthCredentialsWritePath();
|
|
2095
|
+
let raw = {};
|
|
2096
|
+
if (existsSync(credPath)) {
|
|
2097
|
+
raw = JSON.parse(readFileSync(credPath, 'utf-8'));
|
|
2098
|
+
}
|
|
2099
|
+
const existingOauth = raw.claudeAiOauth || {};
|
|
2100
|
+
raw.claudeAiOauth = {
|
|
2101
|
+
...existingOauth,
|
|
2102
|
+
accessToken,
|
|
2103
|
+
refreshToken,
|
|
2104
|
+
expiresAt,
|
|
2105
|
+
scopes,
|
|
2106
|
+
subscriptionType: existingOauth.subscriptionType ?? null,
|
|
2107
|
+
};
|
|
2108
|
+
_saveCredentialsFile(credPath, raw);
|
|
2109
|
+
return {
|
|
2110
|
+
path: credPath,
|
|
2111
|
+
accessToken,
|
|
2112
|
+
refreshToken,
|
|
2113
|
+
expiresAt,
|
|
2114
|
+
scopes,
|
|
2115
|
+
subscriptionType: raw.claudeAiOauth.subscriptionType,
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
export async function beginOAuthLogin() {
|
|
1986
2120
|
const pkce = _oauthGeneratePKCE();
|
|
1987
2121
|
const state = randomBytes(32).toString('base64url');
|
|
1988
|
-
const
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2122
|
+
const buildUrl = (redirectUri) => {
|
|
2123
|
+
const url = new URL(CLAUDE_AI_AUTHORIZE_URL);
|
|
2124
|
+
url.searchParams.set('code', 'true');
|
|
2125
|
+
url.searchParams.set('client_id', CLAUDE_CODE_CLIENT_ID);
|
|
2126
|
+
url.searchParams.set('response_type', 'code');
|
|
2127
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
2128
|
+
url.searchParams.set('scope', OAUTH_LOGIN_SCOPE);
|
|
2129
|
+
url.searchParams.set('code_challenge', pkce.challenge);
|
|
2130
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
2131
|
+
url.searchParams.set('state', state);
|
|
2132
|
+
return url;
|
|
2133
|
+
};
|
|
2134
|
+
const url = buildUrl(OAUTH_REDIRECT_URI);
|
|
2135
|
+
const manualUrl = buildUrl(OAUTH_MANUAL_REDIRECT_URI);
|
|
2136
|
+
|
|
2137
|
+
let server = null;
|
|
2138
|
+
let timeout = null;
|
|
2139
|
+
let finish = null;
|
|
2140
|
+
const waitForCallback = new Promise((resolve, reject) => {
|
|
1999
2141
|
let settled = false;
|
|
2000
|
-
|
|
2001
|
-
const settle = (value, error = null) => {
|
|
2142
|
+
finish = (value, error = null) => {
|
|
2002
2143
|
if (settled) return;
|
|
2003
2144
|
settled = true;
|
|
2004
2145
|
if (timeout) clearTimeout(timeout);
|
|
2005
|
-
try { server
|
|
2146
|
+
try { server?.close(); } catch { /* already closed */ }
|
|
2006
2147
|
if (error) reject(error);
|
|
2007
2148
|
else resolve(value);
|
|
2008
2149
|
};
|
|
2009
|
-
|
|
2150
|
+
server = createServer(async (req, res) => {
|
|
2010
2151
|
const u = new URL(req.url || '/', `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}`);
|
|
2011
2152
|
if (u.pathname !== OAUTH_CALLBACK_PATH) {
|
|
2012
2153
|
res.writeHead(404);
|
|
@@ -2017,76 +2158,24 @@ export async function loginOAuth() {
|
|
|
2017
2158
|
if (!code || u.searchParams.get('state') !== state) {
|
|
2018
2159
|
res.writeHead(400);
|
|
2019
2160
|
res.end('Invalid');
|
|
2020
|
-
|
|
2161
|
+
finish(null);
|
|
2021
2162
|
return;
|
|
2022
2163
|
}
|
|
2023
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
2024
|
-
res.end('<html><body><h2>Claude login successful! You can close this tab.</h2></body></html>');
|
|
2025
2164
|
try {
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
'anthropic-dangerous-direct-browser-access': 'true',
|
|
2031
|
-
'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
|
|
2032
|
-
},
|
|
2033
|
-
body: JSON.stringify({
|
|
2034
|
-
grant_type: 'authorization_code',
|
|
2035
|
-
code,
|
|
2036
|
-
redirect_uri: OAUTH_REDIRECT_URI,
|
|
2037
|
-
client_id: CLAUDE_CODE_CLIENT_ID,
|
|
2038
|
-
code_verifier: pkce.verifier,
|
|
2039
|
-
state,
|
|
2040
|
-
}),
|
|
2041
|
-
redirect: 'error',
|
|
2042
|
-
signal: AbortSignal.timeout(OAUTH_TOKEN_TIMEOUT_MS),
|
|
2043
|
-
dispatcher: getLlmDispatcher(),
|
|
2044
|
-
});
|
|
2045
|
-
if (!tokenRes.ok) {
|
|
2046
|
-
const text = await tokenRes.text().catch(() => '');
|
|
2047
|
-
settle(null, new Error(`[anthropic-oauth] token exchange ${tokenRes.status}: ${_scrubTokens(text).slice(0, 500)}`));
|
|
2048
|
-
return;
|
|
2049
|
-
}
|
|
2050
|
-
const json = await tokenRes.json();
|
|
2051
|
-
const accessToken = json?.access_token || json?.accessToken;
|
|
2052
|
-
const refreshToken = json?.refresh_token || json?.refreshToken;
|
|
2053
|
-
if (!accessToken || !refreshToken) {
|
|
2054
|
-
settle(null, new Error('[anthropic-oauth] token exchange response missing access_token or refresh_token'));
|
|
2055
|
-
return;
|
|
2056
|
-
}
|
|
2057
|
-
const expiresAt = _normalizeExpiresAt(json?.expires_at ?? json?.expiresAt)
|
|
2058
|
-
|| (typeof json?.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0);
|
|
2059
|
-
const scopes = _oauthParseScopeField(json?.scope);
|
|
2060
|
-
const credPath = _oauthCredentialsWritePath();
|
|
2061
|
-
let raw = {};
|
|
2062
|
-
if (existsSync(credPath)) {
|
|
2063
|
-
raw = JSON.parse(readFileSync(credPath, 'utf-8'));
|
|
2064
|
-
}
|
|
2065
|
-
const existingOauth = raw.claudeAiOauth || {};
|
|
2066
|
-
raw.claudeAiOauth = {
|
|
2067
|
-
...existingOauth,
|
|
2068
|
-
accessToken,
|
|
2069
|
-
refreshToken,
|
|
2070
|
-
expiresAt,
|
|
2071
|
-
scopes,
|
|
2072
|
-
subscriptionType: existingOauth.subscriptionType ?? null,
|
|
2073
|
-
};
|
|
2074
|
-
_saveCredentialsFile(credPath, raw);
|
|
2075
|
-
resolve({
|
|
2076
|
-
path: credPath,
|
|
2077
|
-
accessToken,
|
|
2078
|
-
refreshToken,
|
|
2079
|
-
expiresAt,
|
|
2080
|
-
scopes,
|
|
2081
|
-
subscriptionType: raw.claudeAiOauth.subscriptionType,
|
|
2082
|
-
});
|
|
2165
|
+
const tokens = await exchangeAuthorizationCode({ pkce, code, state, redirectUri: OAUTH_REDIRECT_URI });
|
|
2166
|
+
res.writeHead(302, { Location: OAUTH_SUCCESS_REDIRECT_URL });
|
|
2167
|
+
res.end();
|
|
2168
|
+
finish(tokens);
|
|
2083
2169
|
} catch (err) {
|
|
2084
|
-
|
|
2170
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2171
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
2172
|
+
res.end(`Claude login failed: ${error.message}`);
|
|
2173
|
+
finish(null, error);
|
|
2085
2174
|
}
|
|
2086
2175
|
});
|
|
2087
|
-
timeout = setTimeout(() =>
|
|
2176
|
+
timeout = setTimeout(() => finish(null), OAUTH_LOGIN_TIMEOUT_MS);
|
|
2088
2177
|
server.listen(OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_HOST, async () => {
|
|
2089
|
-
process.stderr.write(`\n[anthropic-oauth] Open this URL to log in with Claude:\n${url.toString()}\n\n`);
|
|
2178
|
+
process.stderr.write(`\n[anthropic-oauth] Open this URL to log in with Claude:\n${url.toString()}\n\nIf the localhost callback cannot complete, open this manual URL and paste the shown code#state:\n${manualUrl.toString()}\n\n`);
|
|
2090
2179
|
try {
|
|
2091
2180
|
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
2092
2181
|
openInBrowser(url.toString());
|
|
@@ -2094,8 +2183,31 @@ export async function loginOAuth() {
|
|
|
2094
2183
|
process.stderr.write(`[anthropic-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
2095
2184
|
}
|
|
2096
2185
|
});
|
|
2097
|
-
server.on('error', (err) =>
|
|
2186
|
+
server.on('error', (err) => finish(null, new Error(`[anthropic-oauth] callback server failed on ${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}: ${err?.message || err}`)));
|
|
2098
2187
|
});
|
|
2188
|
+
|
|
2189
|
+
return {
|
|
2190
|
+
provider: 'anthropic-oauth',
|
|
2191
|
+
url: url.toString(),
|
|
2192
|
+
manualUrl: manualUrl.toString(),
|
|
2193
|
+
waitForCallback,
|
|
2194
|
+
completeCode: async (input) => {
|
|
2195
|
+
const parsed = _parseOAuthCodeInput(input);
|
|
2196
|
+
if (parsed.state && parsed.state !== state) throw new Error('[anthropic-oauth] OAuth state mismatch');
|
|
2197
|
+
const redirectUri = parsed.redirectUri || (parsed.state ? OAUTH_MANUAL_REDIRECT_URI : OAUTH_REDIRECT_URI);
|
|
2198
|
+
const tokens = await exchangeAuthorizationCode({ pkce, code: parsed.code, state, redirectUri });
|
|
2199
|
+
finish?.(tokens);
|
|
2200
|
+
return tokens;
|
|
2201
|
+
},
|
|
2202
|
+
cancel: () => {
|
|
2203
|
+
finish?.(null);
|
|
2204
|
+
},
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
export async function loginOAuth() {
|
|
2209
|
+
const login = await beginOAuthLogin();
|
|
2210
|
+
return await login.waitForCallback;
|
|
2099
2211
|
}
|
|
2100
2212
|
|
|
2101
2213
|
// Additive exports for test harnesses.
|