mixdog 0.8.0 → 0.9.0
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 +33 -27
- 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 +344 -0
- 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 +1223 -95
- 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 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- 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 +118 -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 +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -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 +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- 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 +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- 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 +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- 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 +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- 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/snapshot-store.mjs +1 -1
- 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 +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- 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/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- 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 +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -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 +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -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 +145 -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 -98
- 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 +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- 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 +367 -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 +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- 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 +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -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 +746 -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 +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- 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/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/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/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,14 @@ import {
|
|
|
31
30
|
} from '../stall-policy.mjs';
|
|
32
31
|
import {
|
|
33
32
|
classifyError,
|
|
33
|
+
midstreamBackoffFor,
|
|
34
34
|
retryAfterMsFromError,
|
|
35
35
|
withRetry,
|
|
36
36
|
} from './retry-classifier.mjs';
|
|
37
37
|
import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
|
|
38
38
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
39
39
|
import { normalizeContentForAnthropic } from './media-normalization.mjs';
|
|
40
|
+
import { makeInvalidToolArgsMarker } from './openai-compat-stream.mjs';
|
|
40
41
|
|
|
41
42
|
// --- Model catalog cache helpers ---
|
|
42
43
|
// Disk-backed cache so repeated process starts (cron, tool calls) don't
|
|
@@ -45,6 +46,36 @@ const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
|
45
46
|
// SSE progress emits (per-request "Response …" and "Done:" lines). Off by default.
|
|
46
47
|
const SSE_VERBOSE = process.env.MIXDOG_SSE_VERBOSE === '1';
|
|
47
48
|
|
|
49
|
+
/** Bounded mid-stream SSE retries (transient stream loss); shared with anthropic.mjs. */
|
|
50
|
+
export const ANTHROPIC_MAX_MIDSTREAM_RETRIES = 3;
|
|
51
|
+
|
|
52
|
+
function formatRetryAfter(ms) {
|
|
53
|
+
const n = Number(ms);
|
|
54
|
+
if (!Number.isFinite(n) || n < 0) return '';
|
|
55
|
+
if (n >= 60_000 && n % 60_000 === 0) return `${Math.round(n / 60_000)}m`;
|
|
56
|
+
if (n >= 1000) return `${Math.ceil(n / 1000)}s`;
|
|
57
|
+
return `${Math.ceil(n)}ms`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function anthropicQuotaError(status, headers, bodyText = '') {
|
|
61
|
+
const retryAfterMs = retryAfterMsFromError({ headers, response: { headers } });
|
|
62
|
+
const retryAfter = formatRetryAfter(retryAfterMs);
|
|
63
|
+
const detail = bodyText ? `: ${String(bodyText).slice(0, 200)}` : '';
|
|
64
|
+
const retry = retryAfter ? ` retryAfter=${retryAfter}` : '';
|
|
65
|
+
const err = new Error(`Anthropic OAuth API ${status} quota/rate limit${retry}${detail}`);
|
|
66
|
+
err.name = 'ProviderQuotaError';
|
|
67
|
+
err.code = 'PROVIDER_QUOTA';
|
|
68
|
+
err.httpStatus = status;
|
|
69
|
+
err.status = status;
|
|
70
|
+
err.headers = headers;
|
|
71
|
+
err.response = { status, headers };
|
|
72
|
+
err.retryAfterMs = retryAfterMs;
|
|
73
|
+
err.providerQuota = true;
|
|
74
|
+
err.quotaExceeded = true;
|
|
75
|
+
err.unsafeToRetry = true;
|
|
76
|
+
return err;
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
const _modelCache = makeModelCache({
|
|
49
80
|
fileName: 'anthropic-oauth-models.json',
|
|
50
81
|
ttlMs: MODEL_CACHE_TTL_MS,
|
|
@@ -215,10 +246,9 @@ function assertSafeTokenURL(rawURL) {
|
|
|
215
246
|
}
|
|
216
247
|
return rawURL;
|
|
217
248
|
}
|
|
218
|
-
const TOKEN_URL = assertSafeTokenURL(process.env.ANTHROPIC_OAUTH_TOKEN_URL || 'https://
|
|
249
|
+
const TOKEN_URL = assertSafeTokenURL(process.env.ANTHROPIC_OAUTH_TOKEN_URL || 'https://platform.claude.com/v1/oauth/token');
|
|
219
250
|
const ANTHROPIC_VERSION = '2023-06-01';
|
|
220
251
|
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
252
|
const CLAUDE_CODE_CLIENT_ID = process.env.ANTHROPIC_OAUTH_CLIENT_ID || '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
223
253
|
const TOKEN_REFRESH_SKEW_MS = 5 * 60_000;
|
|
224
254
|
const CLAUDE_AI_AUTHORIZE_URL = 'https://claude.com/cai/oauth/authorize';
|
|
@@ -235,15 +265,17 @@ const OAUTH_CALLBACK_HOST = 'localhost';
|
|
|
235
265
|
const OAUTH_CALLBACK_PORT = 54545;
|
|
236
266
|
const OAUTH_CALLBACK_PATH = '/callback';
|
|
237
267
|
const OAUTH_REDIRECT_URI = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}${OAUTH_CALLBACK_PATH}`;
|
|
268
|
+
const OAUTH_MANUAL_REDIRECT_URI = process.env.ANTHROPIC_OAUTH_MANUAL_REDIRECT_URI || 'https://platform.claude.com/oauth/code/callback';
|
|
269
|
+
const OAUTH_SUCCESS_REDIRECT_URL = process.env.ANTHROPIC_OAUTH_SUCCESS_REDIRECT_URL || 'https://platform.claude.com/oauth/code/success?app=claude-code';
|
|
238
270
|
const OAUTH_LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
239
271
|
const OAUTH_TOKEN_TIMEOUT_MS = 30_000;
|
|
240
272
|
|
|
241
|
-
// Anthropic OAuth contract for first-party
|
|
273
|
+
// Anthropic OAuth contract for first-party OAuth clients.
|
|
242
274
|
// Opus/Sonnet requests are gated on a specific system-prompt prefix.
|
|
243
275
|
// Mixdog keeps that upstream client contract for OAuth routing. Haiku is not
|
|
244
276
|
// gated and ignores this prefix.
|
|
245
277
|
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';
|
|
278
|
+
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
279
|
const DEFAULT_CLI_VERSION = '2.1.77';
|
|
248
280
|
|
|
249
281
|
function resolveCliVersion() {
|
|
@@ -252,23 +284,30 @@ function resolveCliVersion() {
|
|
|
252
284
|
}
|
|
253
285
|
|
|
254
286
|
function requiresSystemPrefix(model) {
|
|
255
|
-
// Opus / Sonnet require the
|
|
287
|
+
// Opus / Sonnet require the OAuth system prefix when authenticated
|
|
256
288
|
// via OAuth. Haiku does not.
|
|
257
289
|
return /^claude-(opus|sonnet)/i.test(String(model || ''));
|
|
258
290
|
}
|
|
259
291
|
|
|
260
292
|
// OAuth rate-limit pool routing is gated by the server inspecting the first
|
|
261
|
-
// system block. When it reads exactly
|
|
262
|
-
//
|
|
293
|
+
// system block. When it reads exactly the OAuth system prefix string it routes
|
|
294
|
+
// into the first-party OAuth pool; any other
|
|
263
295
|
// content (even the prefix concatenated with extra text in the same block)
|
|
264
296
|
// falls into the standard pool and Opus/Sonnet return 429. Splitting into
|
|
265
297
|
// 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
|
-
|
|
298
|
+
function buildSystemBlocks(systemMsgs, model, systemTtl, tier3Ttl) {
|
|
299
|
+
// systemMsgs is an array of { content, cacheTier } — each non-empty element
|
|
300
|
+
// becomes its own Anthropic content block with its own cache_control
|
|
301
|
+
// breakpoint. Blocks tagged cacheTier:'tier3' (BP3 sessionMarker) take the
|
|
302
|
+
// tier3 TTL; every other block (BP1 baseRules / BP2 stableSystem) takes the
|
|
303
|
+
// system TTL. Invariant: callers must pass an array.
|
|
304
|
+
const items = Array.isArray(systemMsgs)
|
|
305
|
+
? systemMsgs
|
|
306
|
+
.map(m => ({
|
|
307
|
+
text: typeof m?.content === 'string' ? m.content.trim() : '',
|
|
308
|
+
tier: m?.cacheTier === 'tier3' ? 'tier3' : 'system',
|
|
309
|
+
}))
|
|
310
|
+
.filter(it => it.text)
|
|
272
311
|
: [];
|
|
273
312
|
const gated = requiresSystemPrefix(model);
|
|
274
313
|
|
|
@@ -276,22 +315,24 @@ function buildSystemBlocks(systemText, model, cacheControl, maxCacheBlocks = 2)
|
|
|
276
315
|
if (gated) {
|
|
277
316
|
blocks.push({ type: 'text', text: CLAUDE_CODE_SYSTEM_PREFIX });
|
|
278
317
|
}
|
|
279
|
-
for (let i = 0; i <
|
|
280
|
-
let body =
|
|
281
|
-
// Strip a duplicated
|
|
318
|
+
for (let i = 0; i < items.length; i++) {
|
|
319
|
+
let body = items[i].text;
|
|
320
|
+
// Strip a duplicated OAuth system prefix from the first block if present.
|
|
282
321
|
if (gated && i === 0 && body.startsWith(CLAUDE_CODE_SYSTEM_PREFIX)) {
|
|
283
322
|
body = body.slice(CLAUDE_CODE_SYSTEM_PREFIX.length).trim();
|
|
284
323
|
if (!body) continue;
|
|
285
324
|
}
|
|
286
|
-
blocks.push({ type: 'text', text: body });
|
|
325
|
+
blocks.push({ type: 'text', text: body, _tier: items[i].tier });
|
|
287
326
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
327
|
+
// Apply per-tier cache_control. BP1/BP2 -> systemTtl, BP3 -> tier3Ttl. The
|
|
328
|
+
// gating prefix block is never cached (Anthropic routes on its exact bytes).
|
|
329
|
+
// tier3Ttl === null leaves the 3rd block uncached (e.g. maintenance roles).
|
|
330
|
+
for (const b of blocks) {
|
|
331
|
+
const tier = b._tier;
|
|
332
|
+
delete b._tier;
|
|
333
|
+
if (b.text === CLAUDE_CODE_SYSTEM_PREFIX) continue;
|
|
334
|
+
const ttl = tier === 'tier3' ? tier3Ttl : systemTtl;
|
|
335
|
+
if (ttl) b.cache_control = ttl;
|
|
295
336
|
}
|
|
296
337
|
return blocks;
|
|
297
338
|
}
|
|
@@ -325,6 +366,18 @@ const EFFORT_BUDGET = {
|
|
|
325
366
|
max: 32768,
|
|
326
367
|
};
|
|
327
368
|
|
|
369
|
+
const MIN_THINKING_BUDGET = 1024;
|
|
370
|
+
const THINKING_OUTPUT_RESERVE = 1024;
|
|
371
|
+
|
|
372
|
+
function clampThinkingBudgetTokens(value, maxTokens) {
|
|
373
|
+
const desired = Math.floor(Number(value));
|
|
374
|
+
const max = Math.floor(Number(maxTokens));
|
|
375
|
+
if (!Number.isFinite(desired) || desired <= 0 || !Number.isFinite(max)) return null;
|
|
376
|
+
const ceiling = max - THINKING_OUTPUT_RESERVE;
|
|
377
|
+
if (ceiling < MIN_THINKING_BUDGET) return null;
|
|
378
|
+
return Math.max(MIN_THINKING_BUDGET, Math.min(desired, ceiling));
|
|
379
|
+
}
|
|
380
|
+
|
|
328
381
|
// Tracks which unknown effort labels we've already logged so a repeated
|
|
329
382
|
// session-level misconfig doesn't flood stderr with the same warning.
|
|
330
383
|
const _LOGGED_UNKNOWN_EFFORT = new Set();
|
|
@@ -345,7 +398,6 @@ function credentialCandidates() {
|
|
|
345
398
|
const paths = [];
|
|
346
399
|
_pushUnique(paths, process.env.ANTHROPIC_OAUTH_CREDENTIALS_PATH);
|
|
347
400
|
_pushUnique(paths, DEFAULT_CREDENTIALS_PATH);
|
|
348
|
-
_pushUnique(paths, CLAUDE_CODE_CREDENTIALS_PATH);
|
|
349
401
|
return paths;
|
|
350
402
|
}
|
|
351
403
|
|
|
@@ -385,21 +437,19 @@ function _loadCredentialsFile(path) {
|
|
|
385
437
|
}
|
|
386
438
|
}
|
|
387
439
|
|
|
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.
|
|
440
|
+
// Cross-process safe credential save. Lockfile (O_EXCL) prevents two Mixdog
|
|
441
|
+
// refreshers from clobbering each other; atomic rename guarantees readers see
|
|
442
|
+
// either the old or new file, never a half-written one. Used so refresh_token
|
|
443
|
+
// rotation propagates to other Mixdog readers of the same credentials file
|
|
444
|
+
// instead of leaving them stuck on the previous refresh_token.
|
|
394
445
|
function _saveCredentialsFile(path, raw) {
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode: 0o600 });
|
|
446
|
+
// Secret file, not parent-dir ACL mutation. `secret: true` clamps the file
|
|
447
|
+
// itself on Windows; it deliberately leaves the data dir inheritance alone.
|
|
448
|
+
writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
|
|
399
449
|
}
|
|
400
450
|
|
|
401
|
-
// Cheap stat-only probe so ensureAuth can detect
|
|
402
|
-
//
|
|
451
|
+
// Cheap stat-only probe so ensureAuth can detect Mixdog-updated credentials
|
|
452
|
+
// without paying a full JSON read every call.
|
|
403
453
|
function _credentialsMaxMtime() {
|
|
404
454
|
let max = 0;
|
|
405
455
|
for (const p of credentialCandidates()) {
|
|
@@ -433,7 +483,7 @@ export function describeAnthropicOAuthCredentials() {
|
|
|
433
483
|
try {
|
|
434
484
|
const creds = loadCredentials();
|
|
435
485
|
if (!creds?.accessToken) {
|
|
436
|
-
return { authenticated: false, status: 'Not Set', detail:
|
|
486
|
+
return { authenticated: false, status: 'Not Set', detail: 'Mixdog OAuth credentials' };
|
|
437
487
|
}
|
|
438
488
|
const hasInferenceScope = Array.isArray(creds.scopes) && creds.scopes.includes('user:inference');
|
|
439
489
|
const hasRefresh = Boolean(creds.refreshToken);
|
|
@@ -495,7 +545,7 @@ function _scrubTokens(text) {
|
|
|
495
545
|
|
|
496
546
|
async function refreshOAuthCredentials(creds) {
|
|
497
547
|
if (!creds?.refreshToken) {
|
|
498
|
-
throw new Error('Anthropic OAuth refresh token not available.
|
|
548
|
+
throw new Error('Anthropic OAuth refresh token not available. Open /providers in mixdog to sign in again.');
|
|
499
549
|
}
|
|
500
550
|
|
|
501
551
|
const controller = new AbortController();
|
|
@@ -541,10 +591,10 @@ async function refreshOAuthCredentials(creds) {
|
|
|
541
591
|
scopes: Array.isArray(json?.scope) ? json.scope : creds.scopes,
|
|
542
592
|
subscriptionType: creds.subscriptionType,
|
|
543
593
|
};
|
|
544
|
-
// Persist rotated tokens back so any other reader of the same
|
|
545
|
-
// credentials file picks up the new refresh_token.
|
|
546
|
-
//
|
|
547
|
-
//
|
|
594
|
+
// Persist rotated tokens back so any other Mixdog reader of the same
|
|
595
|
+
// credentials file picks up the new refresh_token. Without this, a
|
|
596
|
+
// later process can replay an old single-use refresh token and loop on
|
|
597
|
+
// invalid_grant.
|
|
548
598
|
if (creds.path && existsSync(creds.path)) {
|
|
549
599
|
try {
|
|
550
600
|
const raw = JSON.parse(readFileSync(creds.path, 'utf-8'));
|
|
@@ -557,8 +607,8 @@ async function refreshOAuthCredentials(creds) {
|
|
|
557
607
|
};
|
|
558
608
|
_saveCredentialsFile(creds.path, raw);
|
|
559
609
|
} catch (err) {
|
|
560
|
-
process.stderr.write(`[anthropic-oauth] credential
|
|
561
|
-
throw new Error(`[oauth] credentials
|
|
610
|
+
process.stderr.write(`[anthropic-oauth] credential save failed: ${_scrubTokens(err?.message || String(err)).slice(0, 200)}\n`);
|
|
611
|
+
throw new Error(`[oauth] credentials save failed: ${err?.message ?? String(err)}`);
|
|
562
612
|
}
|
|
563
613
|
}
|
|
564
614
|
return refreshed;
|
|
@@ -642,9 +692,11 @@ function _sanitizeInputSchema(schema, toolName) {
|
|
|
642
692
|
if (addition) description = description ? `${description} ${addition}` : addition;
|
|
643
693
|
}
|
|
644
694
|
const mergedPropsCount = Object.keys(mergedProps).length;
|
|
645
|
-
process.
|
|
646
|
-
|
|
647
|
-
|
|
695
|
+
if (process.env.MIXDOG_DEBUG_SESSION_LOG) {
|
|
696
|
+
process.stderr.write(
|
|
697
|
+
`[anthropic-oauth-sanitizer] tool="${toolName ?? ''}" compound="${compoundKey}" branches=${Array.isArray(compound) ? compound.length : 0} mergedProps=${mergedPropsCount}\n`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
648
700
|
return {
|
|
649
701
|
type: 'object',
|
|
650
702
|
...(description ? { description } : {}),
|
|
@@ -653,11 +705,28 @@ function _sanitizeInputSchema(schema, toolName) {
|
|
|
653
705
|
}
|
|
654
706
|
|
|
655
707
|
function toAnthropicTools(tools) {
|
|
656
|
-
return tools.map(t =>
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
708
|
+
return tools.map(t => {
|
|
709
|
+
const out = {
|
|
710
|
+
name: t.name,
|
|
711
|
+
description: t.description,
|
|
712
|
+
input_schema: _sanitizeInputSchema(t.inputSchema, t.name),
|
|
713
|
+
};
|
|
714
|
+
if (t.deferLoading === true || t.defer_loading === true) out.defer_loading = true;
|
|
715
|
+
return out;
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
function nativeAnthropicTools(opts) {
|
|
719
|
+
return Array.isArray(opts?.nativeTools)
|
|
720
|
+
? opts.nativeTools.filter(t => t && typeof t === 'object')
|
|
721
|
+
: [];
|
|
722
|
+
}
|
|
723
|
+
function deferredAnthropicTools(activeTools, opts) {
|
|
724
|
+
if (opts?.session?.deferredNativeTools !== true) return [];
|
|
725
|
+
const active = new Set((activeTools || []).map((tool) => String(tool?.name || '').trim()).filter(Boolean));
|
|
726
|
+
const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
|
|
727
|
+
return catalog
|
|
728
|
+
.filter((tool) => tool?.name && !active.has(String(tool.name)))
|
|
729
|
+
.map((tool) => ({ ...tool, deferLoading: true }));
|
|
661
730
|
}
|
|
662
731
|
|
|
663
732
|
function toAnthropicMessages(messages) {
|
|
@@ -694,10 +763,15 @@ function toAnthropicMessages(messages) {
|
|
|
694
763
|
|
|
695
764
|
if (m.role === 'tool') {
|
|
696
765
|
const last = result[result.length - 1];
|
|
766
|
+
const refs = Array.isArray(m.nativeToolSearch?.toolReferences)
|
|
767
|
+
? m.nativeToolSearch.toolReferences.map((name) => String(name || '').trim()).filter(Boolean)
|
|
768
|
+
: [];
|
|
697
769
|
const block = {
|
|
698
770
|
type: 'tool_result',
|
|
699
771
|
tool_use_id: m.toolCallId || '',
|
|
700
|
-
content:
|
|
772
|
+
content: refs.length
|
|
773
|
+
? refs.map((name) => ({ type: 'tool_reference', tool_name: name }))
|
|
774
|
+
: normalizeContentForAnthropic(m.content),
|
|
701
775
|
};
|
|
702
776
|
if (last?.role === 'user' && Array.isArray(last.content)) {
|
|
703
777
|
last.content.push(block);
|
|
@@ -717,17 +791,15 @@ function toAnthropicMessages(messages) {
|
|
|
717
791
|
// sanitizeAnthropicContentPairs has already run (and must NOT run again
|
|
718
792
|
// after this), the blocks we mark here are exactly the blocks the provider
|
|
719
793
|
// 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
794
|
// message-anchor: prefer a safe tool_result tail, then a previous real user
|
|
724
795
|
// text turn if another slot remains. Synthetic
|
|
725
796
|
// <system-reminder> messages and current pure-text prompts
|
|
726
797
|
// are excluded so first-turn prompts do not create a fresh
|
|
727
798
|
// BP4 write on every new session.
|
|
728
|
-
// messageTtl === null disables the tail
|
|
799
|
+
// messageTtl === null disables the tail. BP3 (tier3) now rides a system block,
|
|
800
|
+
// so it is no longer marked here.
|
|
729
801
|
// ANTHROPIC_MSG_SLOTS=0 is honoured upstream by passing messageTtl = null.
|
|
730
|
-
function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_VOLATILE, messageSlots = 1
|
|
802
|
+
function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_VOLATILE, messageSlots = 1 } = {}) {
|
|
731
803
|
if (!Array.isArray(sanitizedMessages) || sanitizedMessages.length === 0) {
|
|
732
804
|
return sanitizedMessages;
|
|
733
805
|
}
|
|
@@ -741,29 +813,20 @@ function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_
|
|
|
741
813
|
return '';
|
|
742
814
|
};
|
|
743
815
|
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
816
|
|
|
749
817
|
const markLast = (msg, ttl) => {
|
|
750
818
|
if (!msg) return;
|
|
751
819
|
msg.content = appendCacheControl(msg.content, ttl);
|
|
752
820
|
};
|
|
753
821
|
const ttlRank = (ttl) => ttl?.ttl === '1h' ? 2 : 1;
|
|
754
|
-
const canMarkMessageIdx = (idx
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
// protects raw/default callers.
|
|
822
|
+
const canMarkMessageIdx = (idx) => {
|
|
823
|
+
// System-reminder messages (volatileTail / roleSpecific BP4) vary
|
|
824
|
+
// per-call, so never pin them with a 1h marker. The 1h system blocks
|
|
825
|
+
// (BP1/BP2/BP3) already satisfy Anthropic's "1h before 5m" ordering.
|
|
759
826
|
if (idx < 0) return false;
|
|
760
827
|
const msg = sanitizedMessages[idx];
|
|
761
828
|
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)) {
|
|
829
|
+
&& isSystemReminder(msg?.content)) {
|
|
767
830
|
return false;
|
|
768
831
|
}
|
|
769
832
|
return true;
|
|
@@ -798,29 +861,16 @@ function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_
|
|
|
798
861
|
return -1;
|
|
799
862
|
};
|
|
800
863
|
|
|
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
864
|
if (messageTtl !== null) {
|
|
815
865
|
const slots = Math.max(0, Math.min(4, Number(messageSlots) || 0));
|
|
816
|
-
const marked = new Set(
|
|
866
|
+
const marked = new Set();
|
|
817
867
|
const candidates = [latestToolResultTailIdx(), previousUserTextAnchorIdx()];
|
|
818
868
|
for (const idx of candidates) {
|
|
819
869
|
if (slots <= 0) break;
|
|
820
|
-
if (idx < 0 || marked.has(idx) || !canMarkMessageIdx(idx
|
|
870
|
+
if (idx < 0 || marked.has(idx) || !canMarkMessageIdx(idx)) continue;
|
|
821
871
|
markLast(sanitizedMessages[idx], messageTtl);
|
|
822
872
|
marked.add(idx);
|
|
823
|
-
if (marked.size
|
|
873
|
+
if (marked.size >= slots) break;
|
|
824
874
|
}
|
|
825
875
|
}
|
|
826
876
|
|
|
@@ -832,13 +882,34 @@ function applyAnthropicCacheMarkers(sanitizedMessages, { messageTtl = CACHE_TTL_
|
|
|
832
882
|
function _captureMidstreamAbort(state, reason) {
|
|
833
883
|
if (!state) return;
|
|
834
884
|
const reasonName = reason?.name || '';
|
|
835
|
-
if (reasonName === '
|
|
885
|
+
if (reasonName === 'AgentStallAbortError' || reasonName === 'StreamStalledAbortError') {
|
|
836
886
|
state.watchdogAbort = reasonName;
|
|
837
887
|
} else {
|
|
838
888
|
state.userAbort = true;
|
|
839
889
|
}
|
|
840
890
|
}
|
|
841
891
|
|
|
892
|
+
async function _midstreamSleepWithAbort(ms, signal) {
|
|
893
|
+
if (!ms) return;
|
|
894
|
+
if (!signal) {
|
|
895
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
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
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
842
913
|
function _statusForAnthropicSseError(type, message) {
|
|
843
914
|
const kind = String(type || '').toLowerCase();
|
|
844
915
|
const text = String(message || '').toLowerCase();
|
|
@@ -894,7 +965,7 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
894
965
|
const resetIdleTimer = () => {
|
|
895
966
|
// OFF by default. When disabled the
|
|
896
967
|
// idle timer never arms, so the stream is never killed on inactivity;
|
|
897
|
-
// the
|
|
968
|
+
// the agent stall watchdog (600s) remains the dead-stream backstop.
|
|
898
969
|
if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED) return;
|
|
899
970
|
if (idleTimer) clearTimeout(idleTimer);
|
|
900
971
|
idleTimer = setTimeout(() => {
|
|
@@ -975,7 +1046,7 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
975
1046
|
if (line.startsWith(':')) {
|
|
976
1047
|
// SSE comment frame (Anthropic `:ping` keepalive). The HTML Standard SSE
|
|
977
1048
|
// spec says comments are silently ignored, but we surface them here so
|
|
978
|
-
// the
|
|
1049
|
+
// the agent stall watchdog sees the stream is still alive during Opus
|
|
979
1050
|
// extended-thinking pauses. No content is emitted — this only refreshes
|
|
980
1051
|
// the runtime's lastStreamDeltaAt timestamp.
|
|
981
1052
|
try { onStreamDelta?.(); } catch {}
|
|
@@ -1062,15 +1133,15 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1062
1133
|
// whole tool_call — the loop never saw it and
|
|
1063
1134
|
// the assistant turn ended with an unmatched
|
|
1064
1135
|
// tool_use id. Wrap the parse so a malformed
|
|
1065
|
-
// input still produces a tool_call (with
|
|
1066
|
-
//
|
|
1067
|
-
// silent drop.
|
|
1136
|
+
// input still produces a tool_call (with an
|
|
1137
|
+
// invalid-args marker and a logged error) instead
|
|
1138
|
+
// of a silent drop or accidental `{}` dispatch.
|
|
1068
1139
|
let parsedArgs = {};
|
|
1069
1140
|
if (pending.inputJson) {
|
|
1070
1141
|
try { parsedArgs = JSON.parse(pending.inputJson); }
|
|
1071
1142
|
catch (parseErr) {
|
|
1072
1143
|
process.stderr.write(`[anthropic-oauth] tool args JSON.parse failed (id=${pending.id}, name=${pending.name}): ${parseErr?.message || parseErr}\n`);
|
|
1073
|
-
parsedArgs =
|
|
1144
|
+
parsedArgs = makeInvalidToolArgsMarker(pending.inputJson, parseErr instanceof Error ? parseErr.message : String(parseErr));
|
|
1074
1145
|
}
|
|
1075
1146
|
}
|
|
1076
1147
|
// Tool arguments must be a plain object. Anthropic's
|
|
@@ -1189,7 +1260,7 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1189
1260
|
*/
|
|
1190
1261
|
export function _classifyMidstreamError(err, state) {
|
|
1191
1262
|
if (!state) return null;
|
|
1192
|
-
if ((state.attemptIndex | 0) >=
|
|
1263
|
+
if ((state.attemptIndex | 0) >= ANTHROPIC_MAX_MIDSTREAM_RETRIES) return null;
|
|
1193
1264
|
if (state.sawCompleted) return null;
|
|
1194
1265
|
if (!state.sawMessageStart) return null;
|
|
1195
1266
|
if (state.userAbort) return null;
|
|
@@ -1200,9 +1271,9 @@ export function _classifyMidstreamError(err, state) {
|
|
|
1200
1271
|
if (status === 401 || status === 403 || status === 429) return null;
|
|
1201
1272
|
|
|
1202
1273
|
const name = err?.name || '';
|
|
1203
|
-
if (name === '
|
|
1274
|
+
if (name === 'AgentStallAbortError') return 'agent_stall';
|
|
1204
1275
|
if (name === 'StreamStalledAbortError') return 'stream_stalled';
|
|
1205
|
-
if (state.watchdogAbort === '
|
|
1276
|
+
if (state.watchdogAbort === 'AgentStallAbortError') return 'agent_stall';
|
|
1206
1277
|
if (state.watchdogAbort === 'StreamStalledAbortError') return 'stream_stalled';
|
|
1207
1278
|
|
|
1208
1279
|
const code = err?.code || err?.cause?.code || '';
|
|
@@ -1232,69 +1303,51 @@ function resolveCacheTtls(opts) {
|
|
|
1232
1303
|
return fallback;
|
|
1233
1304
|
};
|
|
1234
1305
|
// BP budget (4 total):
|
|
1235
|
-
// BP1 baseRules — 1h (shared
|
|
1236
|
-
// BP2
|
|
1237
|
-
// BP3 tier3 — 1h (sessionMarker:
|
|
1306
|
+
// BP1 baseRules — 1h (shared tool policy + compact skill manifest)
|
|
1307
|
+
// BP2 stableSystem — 1h (role/system rules)
|
|
1308
|
+
// BP3 tier3 — 1h (sessionMarker: stable memory/meta body)
|
|
1238
1309
|
// BP4 messages — 1h sliding tail (tool_result cache across iter)
|
|
1239
1310
|
// tools BP is dropped — system BP covers the tools prefix via
|
|
1240
1311
|
// Anthropic's prompt cache prefix semantics (order: tools → system
|
|
1241
1312
|
// → messages).
|
|
1242
1313
|
// tier3 defaults to 1h (stable) — sessionMarker content is stable per
|
|
1243
|
-
//
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
// though their message layout supported it.
|
|
1314
|
+
// memory/meta tuple and the BP slot is only spent when a 3rd
|
|
1315
|
+
// (cacheTier:'tier3') system block is actually present, so this default is
|
|
1316
|
+
// free for sessions that don't carry one. Previously null here meant any
|
|
1317
|
+
// caller that skipped agent runtime resolve (CLI, raw agent spawn)
|
|
1318
|
+
// silently lost the tier3 cache layer even though it supported one.
|
|
1249
1319
|
return {
|
|
1250
|
-
tools: pick('tools',
|
|
1320
|
+
tools: pick('tools', null),
|
|
1251
1321
|
system: pick('system', CACHE_TTL_STABLE),
|
|
1252
1322
|
tier3: pick('tier3', CACHE_TTL_STABLE),
|
|
1253
1323
|
messages: pick('messages', CACHE_TTL_STABLE),
|
|
1254
1324
|
};
|
|
1255
1325
|
}
|
|
1256
1326
|
|
|
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
|
-
}
|
|
1327
|
+
// BP3 (tier3) is injected by session/manager as its own `system` role block —
|
|
1328
|
+
// the 3rd system block, tagged `cacheTier:'tier3'`. buildSystemBlocks applies
|
|
1329
|
+
// the tier3 1h cache_control to that block; BP1/BP2 take the system TTL. No
|
|
1330
|
+
// `<system-reminder>` user message / sentinel scan is involved anymore.
|
|
1276
1331
|
|
|
1277
1332
|
function buildRequestBody(messages, model, tools, sendOpts) {
|
|
1278
1333
|
const systemMsgs = messages.filter(m => m.role === 'system');
|
|
1279
1334
|
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
1335
|
const maxTokens = resolveMaxTokens(model);
|
|
1285
1336
|
const opts = sendOpts || {};
|
|
1286
1337
|
const ttls = resolveCacheTtls(opts);
|
|
1287
|
-
|
|
1338
|
+
// Each system message becomes its own Anthropic content block with its own
|
|
1339
|
+
// breakpoint: BP1 baseRules + BP2 stableSystem at ttls.system, BP3
|
|
1340
|
+
// sessionMarker (cacheTier:'tier3') at ttls.tier3.
|
|
1341
|
+
const systemBlocks = buildSystemBlocks(systemMsgs, model, ttls?.system, ttls?.tier3);
|
|
1288
1342
|
|
|
1289
1343
|
// 4-BP budget layout. tools BP is dropped — system BP covers the
|
|
1290
1344
|
// tools prefix via Anthropic's prompt cache prefix semantics
|
|
1291
|
-
// (order: tools → system → messages). That frees slots for
|
|
1292
|
-
//
|
|
1293
|
-
|
|
1345
|
+
// (order: tools → system → messages). That frees slots for the
|
|
1346
|
+
// messages-tail. The system blocks now hold BP1/BP2/BP3 (tier3), so the
|
|
1347
|
+
// tier3 breakpoint is accounted for inside systemBpUsed.
|
|
1348
|
+
const systemBpUsed = systemBlocks.filter(b => b.cache_control).length;
|
|
1294
1349
|
const toolsBpUsed = 0;
|
|
1295
|
-
const
|
|
1296
|
-
const tier3BpUsed = tier3Idx >= 0 ? 1 : 0;
|
|
1297
|
-
const usedSlots = toolsBpUsed + systemBpUsed + tier3BpUsed;
|
|
1350
|
+
const usedSlots = toolsBpUsed + systemBpUsed;
|
|
1298
1351
|
// Env override for BP strategy. ANTHROPIC_MSG_SLOTS=0 disables message
|
|
1299
1352
|
// caching entirely. Any value >=1 first marks the previous user text turn
|
|
1300
1353
|
// so consecutive requests share a breakpoint; a second free slot marks the
|
|
@@ -1310,10 +1363,9 @@ function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
1310
1363
|
// marked block. NEVER sanitize again after this (see send path).
|
|
1311
1364
|
// msgSlots === 0 (ANTHROPIC_MSG_SLOTS=0, or no free slot) → tail disabled.
|
|
1312
1365
|
const tailTtl = msgSlots > 0 ? ttls.messages : null;
|
|
1313
|
-
const tier3Ttl = tier3Idx >= 0 ? ttls.tier3 : null;
|
|
1314
1366
|
const anthropicMessages = applyAnthropicCacheMarkers(
|
|
1315
1367
|
toAnthropicMessages(chatMsgs),
|
|
1316
|
-
{ messageTtl: tailTtl, messageSlots: msgSlots
|
|
1368
|
+
{ messageTtl: tailTtl, messageSlots: msgSlots },
|
|
1317
1369
|
);
|
|
1318
1370
|
|
|
1319
1371
|
const body = {
|
|
@@ -1325,20 +1377,24 @@ function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
1325
1377
|
|
|
1326
1378
|
if (systemBlocks.length) body.system = systemBlocks;
|
|
1327
1379
|
|
|
1328
|
-
|
|
1380
|
+
const nativeTools = nativeAnthropicTools(opts);
|
|
1381
|
+
const deferredTools = deferredAnthropicTools(tools || [], opts);
|
|
1382
|
+
if (tools?.length || nativeTools.length || deferredTools.length) {
|
|
1329
1383
|
// No cache_control on tools — the systemBase BP already covers the
|
|
1330
1384
|
// tools prefix via Anthropic's prompt cache prefix semantics (order:
|
|
1331
1385
|
// tools → system → messages). Placing a separate BP here would waste
|
|
1332
1386
|
// a slot that's better spent on messages tail.
|
|
1333
|
-
body.tools = toAnthropicTools(tools);
|
|
1387
|
+
body.tools = [...nativeTools, ...toAnthropicTools([...(tools || []), ...deferredTools])];
|
|
1334
1388
|
}
|
|
1335
1389
|
|
|
1336
1390
|
const thinkingBudgetTokens = Number(opts.thinkingBudgetTokens);
|
|
1337
1391
|
if (Number.isFinite(thinkingBudgetTokens) && thinkingBudgetTokens > 0) {
|
|
1338
|
-
|
|
1392
|
+
const budgetTokens = clampThinkingBudgetTokens(thinkingBudgetTokens, maxTokens);
|
|
1393
|
+
if (budgetTokens) body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
|
|
1339
1394
|
} else if (opts.effort) {
|
|
1340
1395
|
if (EFFORT_BUDGET[opts.effort]) {
|
|
1341
|
-
|
|
1396
|
+
const budgetTokens = clampThinkingBudgetTokens(EFFORT_BUDGET[opts.effort], maxTokens);
|
|
1397
|
+
if (budgetTokens) body.thinking = { type: 'enabled', budget_tokens: budgetTokens };
|
|
1342
1398
|
} else if (!_LOGGED_UNKNOWN_EFFORT.has(opts.effort)) {
|
|
1343
1399
|
_LOGGED_UNKNOWN_EFFORT.add(opts.effort);
|
|
1344
1400
|
try {
|
|
@@ -1382,12 +1438,12 @@ export class AnthropicOAuthProvider {
|
|
|
1382
1438
|
this.credentials = loadCredentials();
|
|
1383
1439
|
}
|
|
1384
1440
|
if (!this.credentials) {
|
|
1385
|
-
throw new Error('Anthropic OAuth credentials not found.
|
|
1441
|
+
throw new Error('Anthropic OAuth credentials not found. Open /providers in mixdog to sign in.');
|
|
1386
1442
|
}
|
|
1387
1443
|
|
|
1388
|
-
// Pick up
|
|
1389
|
-
// rewritten — without this, a fresh
|
|
1390
|
-
// the in-memory token's expiry skew triggers a refresh.
|
|
1444
|
+
// Pick up Mixdog-updated tokens the moment the credentials file is
|
|
1445
|
+
// rewritten — without this, a fresh /auth login in another process is
|
|
1446
|
+
// ignored until the in-memory token's expiry skew triggers a refresh.
|
|
1391
1447
|
const diskMtime = _credentialsMaxMtime();
|
|
1392
1448
|
if (diskMtime > 0 && diskMtime > (this.credentials.mtimeMs || 0)) {
|
|
1393
1449
|
const fresh = loadCredentials();
|
|
@@ -1439,7 +1495,7 @@ export class AnthropicOAuthProvider {
|
|
|
1439
1495
|
process.stderr.write(`[anthropic-oauth] WARNING: token expiring but no refresh token; using current token until expiry\n`);
|
|
1440
1496
|
return latest;
|
|
1441
1497
|
}
|
|
1442
|
-
throw new Error('Anthropic OAuth refresh token not available.
|
|
1498
|
+
throw new Error('Anthropic OAuth refresh token not available. Open /providers in mixdog to sign in again.');
|
|
1443
1499
|
}
|
|
1444
1500
|
|
|
1445
1501
|
try {
|
|
@@ -1466,6 +1522,11 @@ export class AnthropicOAuthProvider {
|
|
|
1466
1522
|
}
|
|
1467
1523
|
|
|
1468
1524
|
async send(messages, model, tools, sendOpts) {
|
|
1525
|
+
// Re-warm the kept-alive socket before the turn. preconnect() is a
|
|
1526
|
+
// best-effort no-op while a socket is still hot (TTL gate), but after an
|
|
1527
|
+
// idle gap longer than the keep-alive window it re-opens one in parallel
|
|
1528
|
+
// with auth/body build so the POST below skips the cold TLS handshake.
|
|
1529
|
+
preconnect('https://api.anthropic.com');
|
|
1469
1530
|
// Defense-in-depth: enforce tool_use / tool_result pairing before
|
|
1470
1531
|
// the Anthropic API call. The trim.mjs sanitize pass is normally
|
|
1471
1532
|
// invoked by the budget trimmer in loop.mjs, but dispatches under
|
|
@@ -1502,7 +1563,7 @@ export class AnthropicOAuthProvider {
|
|
|
1502
1563
|
// (PROVIDER_HTTP_RESPONSE_TIMEOUT_MS) for a socket that never sends a
|
|
1503
1564
|
// first byte (truly wedged),
|
|
1504
1565
|
// (b) externalSignal (client disconnect / replaced-by-newer-request), and
|
|
1505
|
-
// (c) the
|
|
1566
|
+
// (c) the agent stall watchdog (STALL_ABORT_S, 600s, progress-based) plus
|
|
1506
1567
|
// the optional SSE idle watchdog for a stream that goes dead mid-flight.
|
|
1507
1568
|
// totalSignal is therefore a pure pass-through of externalSignal with no timer.
|
|
1508
1569
|
const totalTimeout = createPassthroughSignal(externalSignal);
|
|
@@ -1558,6 +1619,7 @@ export class AnthropicOAuthProvider {
|
|
|
1558
1619
|
'anthropic-beta': buildAnthropicBetaHeaders({
|
|
1559
1620
|
base: OAUTH_BETA_HEADERS,
|
|
1560
1621
|
fastMode: this.fastModeBetaHeaderLatched,
|
|
1622
|
+
toolSearch: true,
|
|
1561
1623
|
}),
|
|
1562
1624
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
1563
1625
|
'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
|
|
@@ -1569,7 +1631,7 @@ export class AnthropicOAuthProvider {
|
|
|
1569
1631
|
dispatcher: getLlmDispatcher(),
|
|
1570
1632
|
});
|
|
1571
1633
|
|
|
1572
|
-
|
|
1634
|
+
traceAgentFetch({
|
|
1573
1635
|
sessionId,
|
|
1574
1636
|
headersMs: Date.now() - fetchStartedAt,
|
|
1575
1637
|
httpStatus: response.status,
|
|
@@ -1611,6 +1673,12 @@ export class AnthropicOAuthProvider {
|
|
|
1611
1673
|
const status = Number(result?.response?.status || 0);
|
|
1612
1674
|
const transientStatus = classifyError({ httpStatus: status }) === 'transient';
|
|
1613
1675
|
if (transientStatus || status === 429) {
|
|
1676
|
+
if (status === 429) {
|
|
1677
|
+
const quotaText = await result.response.text().catch(() => '');
|
|
1678
|
+
cleanupCancelHandler(result.cancelHandler);
|
|
1679
|
+
try { result.controller?.abort?.(); } catch {}
|
|
1680
|
+
throw anthropicQuotaError(status, result?.response?.headers, this.scrubTokens(quotaText));
|
|
1681
|
+
}
|
|
1614
1682
|
const err = new Error(`Anthropic OAuth API ${status}`);
|
|
1615
1683
|
err.httpStatus = status;
|
|
1616
1684
|
err.status = status;
|
|
@@ -1642,9 +1710,9 @@ export class AnthropicOAuthProvider {
|
|
|
1642
1710
|
} catch {}
|
|
1643
1711
|
},
|
|
1644
1712
|
});
|
|
1645
|
-
//
|
|
1646
|
-
//
|
|
1647
|
-
const MAX_MIDSTREAM_RETRIES =
|
|
1713
|
+
// Bounded mid-stream retries for transient stream loss; jittered backoff
|
|
1714
|
+
// between attempts (see catch branches).
|
|
1715
|
+
const MAX_MIDSTREAM_RETRIES = ANTHROPIC_MAX_MIDSTREAM_RETRIES;
|
|
1648
1716
|
let firstAttemptError = null;
|
|
1649
1717
|
let firstAttemptClassifier = null;
|
|
1650
1718
|
|
|
@@ -1669,6 +1737,10 @@ export class AnthropicOAuthProvider {
|
|
|
1669
1737
|
const safeText = this.scrubTokens(text).slice(0, 200);
|
|
1670
1738
|
process.stderr.write(`[anthropic-oauth] API error ${response.status}: ${safeText}\n`);
|
|
1671
1739
|
|
|
1740
|
+
if (response.status === 429) {
|
|
1741
|
+
throw anthropicQuotaError(response.status, response.headers, safeText);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1672
1744
|
// Phase I: on unknown/404 model errors, force a catalog refresh and
|
|
1673
1745
|
// retry once. Protects against a silently-rotated model id.
|
|
1674
1746
|
const isUnknownModel = response.status === 404
|
|
@@ -1717,7 +1789,7 @@ export class AnthropicOAuthProvider {
|
|
|
1717
1789
|
|
|
1718
1790
|
const ttftMs = midState.ttftAt ? midState.ttftAt - sseStartedAt : null;
|
|
1719
1791
|
const liveModel = result.model || useModel;
|
|
1720
|
-
|
|
1792
|
+
traceAgentSse({
|
|
1721
1793
|
sessionId,
|
|
1722
1794
|
sseParseMs: Date.now() - sseStartedAt,
|
|
1723
1795
|
ttftMs,
|
|
@@ -1726,7 +1798,7 @@ export class AnthropicOAuthProvider {
|
|
|
1726
1798
|
transport: 'sse',
|
|
1727
1799
|
});
|
|
1728
1800
|
|
|
1729
|
-
|
|
1801
|
+
traceAgentUsage({
|
|
1730
1802
|
sessionId,
|
|
1731
1803
|
iteration,
|
|
1732
1804
|
inputTokens: result.usage?.inputTokens || 0,
|
|
@@ -1799,6 +1871,7 @@ export class AnthropicOAuthProvider {
|
|
|
1799
1871
|
firstAttemptClassifier = 'empty_stream';
|
|
1800
1872
|
try { controller?.abort?.(err); } catch { /* best-effort teardown */ }
|
|
1801
1873
|
try { process.stderr.write(`[anthropic-oauth] empty stream (no message_start) — retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES}\n`); } catch {}
|
|
1874
|
+
await _midstreamSleepWithAbort(midstreamBackoffFor(attemptIndex + 1), totalSignal);
|
|
1802
1875
|
continue;
|
|
1803
1876
|
}
|
|
1804
1877
|
if (classifyError(err) === 'transient'
|
|
@@ -1811,6 +1884,7 @@ export class AnthropicOAuthProvider {
|
|
|
1811
1884
|
try {
|
|
1812
1885
|
process.stderr.write(`[anthropic-oauth] transient SSE error — retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES} (${err?.providerErrorType || err?.message || 'unknown'})\n`);
|
|
1813
1886
|
} catch {}
|
|
1887
|
+
await _midstreamSleepWithAbort(midstreamBackoffFor(attemptIndex + 1), totalSignal);
|
|
1814
1888
|
continue;
|
|
1815
1889
|
}
|
|
1816
1890
|
// Truncated stream (message_start without message_stop): the
|
|
@@ -1832,6 +1906,7 @@ export class AnthropicOAuthProvider {
|
|
|
1832
1906
|
firstAttemptClassifier = 'truncated_stream';
|
|
1833
1907
|
try { controller?.abort?.(err); } catch { /* best-effort teardown */ }
|
|
1834
1908
|
try { process.stderr.write(`[anthropic-oauth] truncated stream — retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES}\n`); } catch {}
|
|
1909
|
+
await _midstreamSleepWithAbort(midstreamBackoffFor(attemptIndex + 1), totalSignal);
|
|
1835
1910
|
continue;
|
|
1836
1911
|
}
|
|
1837
1912
|
const classifier = _classifyMidstreamError(err, midState);
|
|
@@ -1845,6 +1920,7 @@ export class AnthropicOAuthProvider {
|
|
|
1845
1920
|
try {
|
|
1846
1921
|
process.stderr.write(`[anthropic-oauth] mid-stream recovered: retry ${attemptIndex + 1}/${MAX_MIDSTREAM_RETRIES} (cause: ${classifier})\n`);
|
|
1847
1922
|
} catch {}
|
|
1923
|
+
await _midstreamSleepWithAbort(midstreamBackoffFor(attemptIndex + 1), totalSignal);
|
|
1848
1924
|
continue;
|
|
1849
1925
|
}
|
|
1850
1926
|
if (attemptIndex > 0 && firstAttemptError) {
|
|
@@ -1982,31 +2058,118 @@ function _oauthParseScopeField(scope) {
|
|
|
1982
2058
|
return String(scope || '').split(' ').filter(Boolean);
|
|
1983
2059
|
}
|
|
1984
2060
|
|
|
1985
|
-
|
|
2061
|
+
function _parseOAuthCodeInput(input) {
|
|
2062
|
+
const value = String(input || '').trim();
|
|
2063
|
+
if (!value) return { code: '', state: '' };
|
|
2064
|
+
try {
|
|
2065
|
+
const url = new URL(value);
|
|
2066
|
+
const code = url.searchParams.get('code') || '';
|
|
2067
|
+
const state = url.searchParams.get('state') || '';
|
|
2068
|
+
if (code || state) return { code, state, redirectUri: `${url.origin}${url.pathname}` };
|
|
2069
|
+
} catch { /* not a URL */ }
|
|
2070
|
+
if (value.includes('#')) {
|
|
2071
|
+
const [code, state] = value.split('#', 2);
|
|
2072
|
+
return { code: String(code || '').trim(), state: String(state || '').trim() };
|
|
2073
|
+
}
|
|
2074
|
+
if (value.includes('code=')) {
|
|
2075
|
+
const params = new URLSearchParams(value.startsWith('?') ? value.slice(1) : value);
|
|
2076
|
+
return { code: params.get('code') || '', state: params.get('state') || '' };
|
|
2077
|
+
}
|
|
2078
|
+
return { code: value, state: '' };
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
async function exchangeAuthorizationCode({ pkce, code, state, redirectUri }) {
|
|
2082
|
+
const cleanCode = String(code || '').trim();
|
|
2083
|
+
if (!cleanCode) throw new Error('[anthropic-oauth] authorization code is required');
|
|
2084
|
+
const tokenRes = await fetch(TOKEN_URL, {
|
|
2085
|
+
method: 'POST',
|
|
2086
|
+
headers: {
|
|
2087
|
+
'Content-Type': 'application/json',
|
|
2088
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
2089
|
+
'user-agent': `claude-cli/${resolveCliVersion()} (external, sdk-cli)`,
|
|
2090
|
+
},
|
|
2091
|
+
body: JSON.stringify({
|
|
2092
|
+
grant_type: 'authorization_code',
|
|
2093
|
+
code: cleanCode,
|
|
2094
|
+
redirect_uri: redirectUri,
|
|
2095
|
+
client_id: CLAUDE_CODE_CLIENT_ID,
|
|
2096
|
+
code_verifier: pkce.verifier,
|
|
2097
|
+
state,
|
|
2098
|
+
}),
|
|
2099
|
+
redirect: 'error',
|
|
2100
|
+
signal: AbortSignal.timeout(OAUTH_TOKEN_TIMEOUT_MS),
|
|
2101
|
+
dispatcher: getLlmDispatcher(),
|
|
2102
|
+
});
|
|
2103
|
+
if (!tokenRes.ok) {
|
|
2104
|
+
const text = await tokenRes.text().catch(() => '');
|
|
2105
|
+
throw new Error(`[anthropic-oauth] token exchange ${tokenRes.status}: ${_scrubTokens(text).slice(0, 500)}`);
|
|
2106
|
+
}
|
|
2107
|
+
const json = await tokenRes.json();
|
|
2108
|
+
const accessToken = json?.access_token || json?.accessToken;
|
|
2109
|
+
const refreshToken = json?.refresh_token || json?.refreshToken;
|
|
2110
|
+
if (!accessToken || !refreshToken) {
|
|
2111
|
+
throw new Error('[anthropic-oauth] token exchange response missing access_token or refresh_token');
|
|
2112
|
+
}
|
|
2113
|
+
const expiresAt = _normalizeExpiresAt(json?.expires_at ?? json?.expiresAt)
|
|
2114
|
+
|| (typeof json?.expires_in === 'number' ? Date.now() + json.expires_in * 1000 : 0);
|
|
2115
|
+
const scopes = _oauthParseScopeField(json?.scope);
|
|
2116
|
+
const credPath = _oauthCredentialsWritePath();
|
|
2117
|
+
let raw = {};
|
|
2118
|
+
if (existsSync(credPath)) {
|
|
2119
|
+
raw = JSON.parse(readFileSync(credPath, 'utf-8'));
|
|
2120
|
+
}
|
|
2121
|
+
const existingOauth = raw.claudeAiOauth || {};
|
|
2122
|
+
raw.claudeAiOauth = {
|
|
2123
|
+
...existingOauth,
|
|
2124
|
+
accessToken,
|
|
2125
|
+
refreshToken,
|
|
2126
|
+
expiresAt,
|
|
2127
|
+
scopes,
|
|
2128
|
+
subscriptionType: existingOauth.subscriptionType ?? null,
|
|
2129
|
+
};
|
|
2130
|
+
_saveCredentialsFile(credPath, raw);
|
|
2131
|
+
return {
|
|
2132
|
+
path: credPath,
|
|
2133
|
+
accessToken,
|
|
2134
|
+
refreshToken,
|
|
2135
|
+
expiresAt,
|
|
2136
|
+
scopes,
|
|
2137
|
+
subscriptionType: raw.claudeAiOauth.subscriptionType,
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
export async function beginOAuthLogin() {
|
|
1986
2142
|
const pkce = _oauthGeneratePKCE();
|
|
1987
2143
|
const state = randomBytes(32).toString('base64url');
|
|
1988
|
-
const
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2144
|
+
const buildUrl = (redirectUri) => {
|
|
2145
|
+
const url = new URL(CLAUDE_AI_AUTHORIZE_URL);
|
|
2146
|
+
url.searchParams.set('code', 'true');
|
|
2147
|
+
url.searchParams.set('client_id', CLAUDE_CODE_CLIENT_ID);
|
|
2148
|
+
url.searchParams.set('response_type', 'code');
|
|
2149
|
+
url.searchParams.set('redirect_uri', redirectUri);
|
|
2150
|
+
url.searchParams.set('scope', OAUTH_LOGIN_SCOPE);
|
|
2151
|
+
url.searchParams.set('code_challenge', pkce.challenge);
|
|
2152
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
2153
|
+
url.searchParams.set('state', state);
|
|
2154
|
+
return url;
|
|
2155
|
+
};
|
|
2156
|
+
const url = buildUrl(OAUTH_REDIRECT_URI);
|
|
2157
|
+
const manualUrl = buildUrl(OAUTH_MANUAL_REDIRECT_URI);
|
|
2158
|
+
|
|
2159
|
+
let server = null;
|
|
2160
|
+
let timeout = null;
|
|
2161
|
+
let finish = null;
|
|
2162
|
+
const waitForCallback = new Promise((resolve, reject) => {
|
|
1999
2163
|
let settled = false;
|
|
2000
|
-
|
|
2001
|
-
const settle = (value, error = null) => {
|
|
2164
|
+
finish = (value, error = null) => {
|
|
2002
2165
|
if (settled) return;
|
|
2003
2166
|
settled = true;
|
|
2004
2167
|
if (timeout) clearTimeout(timeout);
|
|
2005
|
-
try { server
|
|
2168
|
+
try { server?.close(); } catch { /* already closed */ }
|
|
2006
2169
|
if (error) reject(error);
|
|
2007
2170
|
else resolve(value);
|
|
2008
2171
|
};
|
|
2009
|
-
|
|
2172
|
+
server = createServer(async (req, res) => {
|
|
2010
2173
|
const u = new URL(req.url || '/', `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}`);
|
|
2011
2174
|
if (u.pathname !== OAUTH_CALLBACK_PATH) {
|
|
2012
2175
|
res.writeHead(404);
|
|
@@ -2017,76 +2180,24 @@ export async function loginOAuth() {
|
|
|
2017
2180
|
if (!code || u.searchParams.get('state') !== state) {
|
|
2018
2181
|
res.writeHead(400);
|
|
2019
2182
|
res.end('Invalid');
|
|
2020
|
-
|
|
2183
|
+
finish(null);
|
|
2021
2184
|
return;
|
|
2022
2185
|
}
|
|
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
2186
|
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
|
-
});
|
|
2187
|
+
const tokens = await exchangeAuthorizationCode({ pkce, code, state, redirectUri: OAUTH_REDIRECT_URI });
|
|
2188
|
+
res.writeHead(302, { Location: OAUTH_SUCCESS_REDIRECT_URL });
|
|
2189
|
+
res.end();
|
|
2190
|
+
finish(tokens);
|
|
2083
2191
|
} catch (err) {
|
|
2084
|
-
|
|
2192
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
2193
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
2194
|
+
res.end(`Claude login failed: ${error.message}`);
|
|
2195
|
+
finish(null, error);
|
|
2085
2196
|
}
|
|
2086
2197
|
});
|
|
2087
|
-
timeout = setTimeout(() =>
|
|
2198
|
+
timeout = setTimeout(() => finish(null), OAUTH_LOGIN_TIMEOUT_MS);
|
|
2088
2199
|
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`);
|
|
2200
|
+
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
2201
|
try {
|
|
2091
2202
|
const { openInBrowser } = await import('../../../shared/open-url.mjs');
|
|
2092
2203
|
openInBrowser(url.toString());
|
|
@@ -2094,8 +2205,31 @@ export async function loginOAuth() {
|
|
|
2094
2205
|
process.stderr.write(`[anthropic-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
|
|
2095
2206
|
}
|
|
2096
2207
|
});
|
|
2097
|
-
server.on('error', (err) =>
|
|
2208
|
+
server.on('error', (err) => finish(null, new Error(`[anthropic-oauth] callback server failed on ${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}: ${err?.message || err}`)));
|
|
2098
2209
|
});
|
|
2210
|
+
|
|
2211
|
+
return {
|
|
2212
|
+
provider: 'anthropic-oauth',
|
|
2213
|
+
url: url.toString(),
|
|
2214
|
+
manualUrl: manualUrl.toString(),
|
|
2215
|
+
waitForCallback,
|
|
2216
|
+
completeCode: async (input) => {
|
|
2217
|
+
const parsed = _parseOAuthCodeInput(input);
|
|
2218
|
+
if (parsed.state && parsed.state !== state) throw new Error('[anthropic-oauth] OAuth state mismatch');
|
|
2219
|
+
const redirectUri = parsed.redirectUri || (parsed.state ? OAUTH_MANUAL_REDIRECT_URI : OAUTH_REDIRECT_URI);
|
|
2220
|
+
const tokens = await exchangeAuthorizationCode({ pkce, code: parsed.code, state, redirectUri });
|
|
2221
|
+
finish?.(tokens);
|
|
2222
|
+
return tokens;
|
|
2223
|
+
},
|
|
2224
|
+
cancel: () => {
|
|
2225
|
+
finish?.(null);
|
|
2226
|
+
},
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
export async function loginOAuth() {
|
|
2231
|
+
const login = await beginOAuthLogin();
|
|
2232
|
+
return await login.waitForCallback;
|
|
2099
2233
|
}
|
|
2100
2234
|
|
|
2101
2235
|
// Additive exports for test harnesses.
|