mixdog 0.9.147 → 0.9.148
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +2 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +17 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +20 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +132 -42
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +58 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +45 -8
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +178 -29
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +48 -66
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
|
@@ -8,7 +8,7 @@ import { randomBytes } from 'crypto';
|
|
|
8
8
|
import {
|
|
9
9
|
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
10
10
|
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
11
|
-
|
|
11
|
+
PROVIDER_SSE_IDLE_TIMEOUT_MS,
|
|
12
12
|
streamStalledError,
|
|
13
13
|
} from '../stall-policy.mjs';
|
|
14
14
|
import {
|
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
} from './lib/anthropic-native-blocks.mjs';
|
|
26
26
|
import { parseProviderJsonBatch } from './stream-json-pool.mjs';
|
|
27
27
|
import { stampStreamOutcome, STREAM_TRANSPORTS, STREAM_OUTCOME_VERSION } from './lib/stream-outcome.mjs';
|
|
28
|
+
import {
|
|
29
|
+
anthropicFallbackProviderMetadata,
|
|
30
|
+
parseAnthropicFallbackBlock,
|
|
31
|
+
} from './anthropic-server-fallback.mjs';
|
|
28
32
|
|
|
29
33
|
/** Bounded mid-stream SSE retries (transient stream loss); shared with anthropic.mjs.
|
|
30
34
|
* Sourced from the single shared retry-budget table (MIDSTREAM_RETRY_POLICY.sse). */
|
|
@@ -132,20 +136,23 @@ function cloneNativeBlock(block) {
|
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
export async function parseSSEStream(response, signal, abortStream, onStreamDelta, onToolCall, state, onTextDelta, knownToolNames) {
|
|
135
|
-
//
|
|
136
|
-
//
|
|
139
|
+
// Anthropic/Claude parity: every received SSE byte proves transport
|
|
140
|
+
// activity, including comment and named ping keepalives. Content kinds
|
|
141
|
+
// remain distinct on onStreamDelta so TTFT and visible-progress accounting
|
|
142
|
+
// do not mistake transport heartbeats for model output.
|
|
137
143
|
const reader = response.body.getReader();
|
|
138
144
|
const decoder = new TextDecoder();
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
// Transport-idle window. The legacy semanticIdleTimeoutMs seam remains as
|
|
146
|
+
// a fallback for existing tests/callers, but production uses the shared
|
|
147
|
+
// byte/event inactivity policy.
|
|
148
|
+
const configuredIdleTimeoutMs = state?.transportIdleTimeoutMs ?? state?.semanticIdleTimeoutMs;
|
|
149
|
+
const SSE_IDLE_TIMEOUT_MS = Number.isFinite(Number(configuredIdleTimeoutMs))
|
|
150
|
+
&& Number(configuredIdleTimeoutMs) > 0
|
|
151
|
+
? Number(configuredIdleTimeoutMs)
|
|
152
|
+
: PROVIDER_SSE_IDLE_TIMEOUT_MS;
|
|
153
|
+
const idleWatchdogEnabled = typeof state?.transportIdleWatchdogEnabled === 'boolean'
|
|
154
|
+
? state.transportIdleWatchdogEnabled
|
|
155
|
+
: PROVIDER_SSE_IDLE_WATCHDOG_ENABLED;
|
|
149
156
|
const SSE_FIRST_MESSAGE_TIMEOUT_MS = Number.isFinite(Number(state?.firstMessageTimeoutMs))
|
|
150
157
|
&& Number(state.firstMessageTimeoutMs) > 0
|
|
151
158
|
? Number(state.firstMessageTimeoutMs)
|
|
@@ -215,6 +222,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
215
222
|
let usage = { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, raw: null };
|
|
216
223
|
let stopReason = null;
|
|
217
224
|
let stopDetails;
|
|
225
|
+
const fallbackEvents = [];
|
|
218
226
|
let buffer = '';
|
|
219
227
|
let idleTimedOut = false;
|
|
220
228
|
let firstMessageTimedOut = false;
|
|
@@ -389,10 +397,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
389
397
|
};
|
|
390
398
|
|
|
391
399
|
const resetIdleTimer = () => {
|
|
392
|
-
|
|
393
|
-
// idle timer never arms, so the stream is never killed on inactivity;
|
|
394
|
-
// the agent stall watchdog (600s) remains the dead-stream backstop.
|
|
395
|
-
if (!PROVIDER_SSE_IDLE_WATCHDOG_ENABLED) return;
|
|
400
|
+
if (!idleWatchdogEnabled) return;
|
|
396
401
|
if (idleTimer) clearTimeout(idleTimer);
|
|
397
402
|
idleTimer = setTimeout(() => {
|
|
398
403
|
idleTimedOut = true;
|
|
@@ -413,9 +418,8 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
413
418
|
const e = _attachStallPartial(streamStalledError('Anthropic OAuth SSE', SSE_IDLE_TIMEOUT_MS, { emittedToolCall: !!state?.emittedToolCall }));
|
|
414
419
|
const r = idleReject; idleReject = null; r(e);
|
|
415
420
|
}
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
// while `:ping` keepalives keep the transport socket warm.
|
|
421
|
+
// Only actual transport silence trips this. Anthropic keepalives prove
|
|
422
|
+
// that the generation connection is still alive.
|
|
419
423
|
}, SSE_IDLE_TIMEOUT_MS);
|
|
420
424
|
try { idleTimer.unref?.(); } catch {}
|
|
421
425
|
};
|
|
@@ -438,11 +442,11 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
438
442
|
}
|
|
439
443
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
440
444
|
}
|
|
441
|
-
//
|
|
445
|
+
// Do not arm the transport-idle timer before the
|
|
442
446
|
// stream has produced its first event. A slow first response is governed
|
|
443
447
|
// by armFirstMessageTimer() (first-byte window) alone; arming the
|
|
444
|
-
//
|
|
445
|
-
// first response as a stall. The
|
|
448
|
+
// transport idle here could let it win and mis-abort a legitimately slow
|
|
449
|
+
// first response as a stall. The transport idle is first armed at
|
|
446
450
|
// `message_start` (see below), so it only ever guards MID-stream silence.
|
|
447
451
|
armFirstMessageTimer();
|
|
448
452
|
streamLoop: while (true) {
|
|
@@ -471,18 +475,17 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
471
475
|
}
|
|
472
476
|
const { done, value } = chunk;
|
|
473
477
|
if (done) break;
|
|
478
|
+
if (state?.sawMessageStart) resetIdleTimer();
|
|
479
|
+
try { onStreamDelta?.('transport'); } catch {}
|
|
474
480
|
buffer += decoder.decode(value, { stream: true });
|
|
475
481
|
const lines = buffer.split('\n');
|
|
476
482
|
buffer = lines.pop() || '';
|
|
477
483
|
|
|
478
484
|
for (const line of lines) {
|
|
479
485
|
if (line.startsWith(':')) {
|
|
480
|
-
// SSE comment frame (Anthropic `:ping` keepalive).
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
// can look alive forever without message_start/content.
|
|
484
|
-
// Crucially it also does NOT reset the SEMANTIC idle timer
|
|
485
|
-
// below — a ping-only wedge must trip the idle abort.
|
|
486
|
+
// SSE comment frame (Anthropic `:ping` keepalive). The raw
|
|
487
|
+
// chunk already refreshed transport activity above; it is
|
|
488
|
+
// intentionally not counted as visible model progress.
|
|
486
489
|
continue;
|
|
487
490
|
}
|
|
488
491
|
// Blank lines are SSE record separators — emitted after EVERY
|
|
@@ -504,18 +507,6 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
504
507
|
// cancellation after this frame is relayed.
|
|
505
508
|
const [event] = await parseProviderJsonBatch([data]);
|
|
506
509
|
|
|
507
|
-
// SEMANTIC idle reset (Part A): reset the idle timer ONLY for
|
|
508
|
-
// real progress events, NOT for Anthropic keepalives. Anthropic
|
|
509
|
-
// sends pings as a NAMED event (`event: ping` /
|
|
510
|
-
// `data: {"type":"ping"}`), not just `:` comment frames, so a
|
|
511
|
-
// named ping must be excluded here or a ping-only wedge keeps
|
|
512
|
-
// the timer alive forever. Everything that is not a ping is a
|
|
513
|
-
// genuine server event (message_start/content/tool/thinking
|
|
514
|
-
// deltas, message_delta/stop, errors) and counts as progress.
|
|
515
|
-
if (currentEvent !== 'ping' && event?.type !== 'ping') {
|
|
516
|
-
resetIdleTimer();
|
|
517
|
-
}
|
|
518
|
-
|
|
519
510
|
if (currentEvent === 'error' || event?.type === 'error' || event?.error) {
|
|
520
511
|
throw _anthropicSseError(event);
|
|
521
512
|
}
|
|
@@ -523,12 +514,9 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
523
514
|
if (event.type === 'message_start' && event.message) {
|
|
524
515
|
clearFirstMessageTimer();
|
|
525
516
|
if (state) state.sawMessageStart = true;
|
|
526
|
-
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
// semantic message boundary. Content kinds remain
|
|
530
|
-
// reasoning/text/tool-specific below.
|
|
531
|
-
try { onStreamDelta?.('transport'); } catch {}
|
|
517
|
+
resetIdleTimer();
|
|
518
|
+
// Transport activity was already reported for the raw
|
|
519
|
+
// chunk; this reports the semantic message boundary.
|
|
532
520
|
try { onStreamDelta?.('semantic'); } catch {}
|
|
533
521
|
if (event.message.model) model = event.message.model;
|
|
534
522
|
if (event.message.usage) {
|
|
@@ -541,6 +529,13 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
541
529
|
|
|
542
530
|
if (event.type === 'content_block_start') {
|
|
543
531
|
const block = event.content_block;
|
|
532
|
+
const fallback = parseAnthropicFallbackBlock(block);
|
|
533
|
+
if (fallback) {
|
|
534
|
+
fallbackEvents.push(fallback);
|
|
535
|
+
model = fallback.fallbackModel;
|
|
536
|
+
contentBlockTypes.add('fallback');
|
|
537
|
+
try { onStreamDelta?.('semantic'); } catch {}
|
|
538
|
+
}
|
|
544
539
|
if (block?.type === 'tool_use') {
|
|
545
540
|
if (state) state.partialToolCall = true;
|
|
546
541
|
pendingToolInputs.set(event.index, {
|
|
@@ -757,7 +752,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
757
752
|
// An invalid-args marker never fingerprint-collides with
|
|
758
753
|
// a real recovered call, so malformed native calls still
|
|
759
754
|
// dispatch (the marker path is unaffected).
|
|
760
|
-
if (_toolDedupe.shouldDispatch(call.name, call.arguments)) {
|
|
755
|
+
if (_toolDedupe.shouldDispatch(call.name, call.arguments, call.id)) {
|
|
761
756
|
toolCalls.push(call);
|
|
762
757
|
// Ordered replay copy of the dispatched call —
|
|
763
758
|
// skipped/deduped calls stay out so a replayed
|
|
@@ -947,6 +942,7 @@ export async function parseSSEStream(response, signal, abortStream, onStreamDelt
|
|
|
947
942
|
// assistantBlocks branch). Absent for every ordinary turn, which
|
|
948
943
|
// keeps the existing text/thinking/tool_use lowering untouched.
|
|
949
944
|
assistantBlocks: _orderedAssistantBlocks(),
|
|
945
|
+
providerMetadata: anthropicFallbackProviderMetadata(fallbackEvents),
|
|
950
946
|
};
|
|
951
947
|
} finally {
|
|
952
948
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
anthropicMaxAttempts,
|
|
9
9
|
anthropicRequestTimeoutMs,
|
|
10
10
|
classifyError,
|
|
11
|
-
|
|
11
|
+
markProviderRecoveryExhausted,
|
|
12
|
+
resolveStallRetryBudget,
|
|
12
13
|
midstreamBackoffFor,
|
|
13
14
|
sleepWithAbort,
|
|
14
15
|
STREAM_STALL_RETRY_BUDGET_MS,
|
|
@@ -31,6 +32,8 @@ import {
|
|
|
31
32
|
stampAnthropicStreamOutcome,
|
|
32
33
|
} from './anthropic-sse.mjs';
|
|
33
34
|
import { buildAnthropicBetaHeaders, supportsAnthropicFastMode } from './anthropic-betas.mjs';
|
|
35
|
+
import { applyAnthropicServerFallback } from './anthropic-server-fallback.mjs';
|
|
36
|
+
import { fastModeAvailable, noteFastModeCapacityError } from './anthropic-fast-mode.mjs';
|
|
34
37
|
import {
|
|
35
38
|
applyAnthropicEffortToBody,
|
|
36
39
|
effortValuesForModel,
|
|
@@ -52,6 +55,7 @@ import {
|
|
|
52
55
|
requestAnthropicTools as sharedRequestAnthropicTools,
|
|
53
56
|
normalizeAnthropicNonStreamingResponse,
|
|
54
57
|
resolveAnthropicCacheTtls as resolveCacheTtls,
|
|
58
|
+
resolveAnthropicMessageCacheSlots,
|
|
55
59
|
sanitizeAnthropicInputSchema,
|
|
56
60
|
toAnthropicToolChoice,
|
|
57
61
|
} from './lib/anthropic-request-utils.mjs';
|
|
@@ -138,30 +142,16 @@ export class AnthropicProvider {
|
|
|
138
142
|
// (cacheTier:'tier3') at ttls.tier3 — each its own system content block.
|
|
139
143
|
const systemBlocks = buildSystemBlocks(systemMsgs, ttls.system, ttls.tier3);
|
|
140
144
|
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
// messages-tail.
|
|
145
|
-
const toolsBpUsed = 0;
|
|
146
|
-
const systemBpUsed = systemBlocks.filter(b => b.cache_control).length;
|
|
147
|
-
const usedSlots = toolsBpUsed + systemBpUsed;
|
|
148
|
-
// Env override for BP strategy. ANTHROPIC_MSG_SLOTS=0 disables
|
|
149
|
-
// message caching entirely. Any value >=1 first marks the previous
|
|
150
|
-
// user text turn; a second free slot marks the tail.
|
|
151
|
-
const msgSlotsCap = Number.parseInt(process.env.ANTHROPIC_MSG_SLOTS, 10);
|
|
152
|
-
const defaultMsgSlots = Math.max(0, 4 - usedSlots);
|
|
153
|
-
const msgSlots = ttls.messages
|
|
154
|
-
? (Number.isFinite(msgSlotsCap) && msgSlotsCap >= 0 ? Math.min(msgSlotsCap, defaultMsgSlots) : defaultMsgSlots)
|
|
155
|
-
: 0;
|
|
145
|
+
// Message-tail cache budget (4-BP layout + ANTHROPIC_MSG_SLOTS) is
|
|
146
|
+
// shared with anthropic-oauth — see resolveAnthropicMessageCacheSlots.
|
|
147
|
+
const messageCacheSlots = resolveAnthropicMessageCacheSlots(systemBlocks, ttls);
|
|
156
148
|
// Build → sanitize (once, inside toAnthropicMessages) → mark. Markers
|
|
157
149
|
// are applied to the FINAL sanitized array by invariant, so block
|
|
158
150
|
// drops / inserts / reorders performed by the sanitizer can never move
|
|
159
151
|
// or delete a marked block. NEVER sanitize again after this.
|
|
160
|
-
// msgSlots === 0 → message-tail disabled.
|
|
161
|
-
const tailTtl = msgSlots > 0 ? ttls.messages : null;
|
|
162
152
|
const anthropicMessages = applyAnthropicCacheMarkers(
|
|
163
153
|
toAnthropicMessages(chatMsgs),
|
|
164
|
-
|
|
154
|
+
messageCacheSlots,
|
|
165
155
|
);
|
|
166
156
|
|
|
167
157
|
const params = {
|
|
@@ -170,6 +160,9 @@ export class AnthropicProvider {
|
|
|
170
160
|
system: systemBlocks.length ? systemBlocks : undefined,
|
|
171
161
|
messages: anthropicMessages,
|
|
172
162
|
};
|
|
163
|
+
applyAnthropicServerFallback(params, useModel, {
|
|
164
|
+
enabled: this.config?.disableBetaHeaders !== true && opts.serverFallback !== false,
|
|
165
|
+
});
|
|
173
166
|
const requestTools = requestAnthropicTools(tools, chatMsgs, opts);
|
|
174
167
|
if (requestTools.length) {
|
|
175
168
|
// No cache_control on tools — the system BP covers tools via
|
|
@@ -201,7 +194,8 @@ export class AnthropicProvider {
|
|
|
201
194
|
logTag: this.name,
|
|
202
195
|
});
|
|
203
196
|
// Fast mode → speed: "fast" on models Anthropic marks as speed-capable.
|
|
204
|
-
|
|
197
|
+
// Suppressed while the fast capacity pool is cooling down.
|
|
198
|
+
if (opts.fast === true && supportsAnthropicFastMode(useModel) && fastModeAvailable()) {
|
|
205
199
|
params.speed = 'fast';
|
|
206
200
|
this.fastModeBetaHeaderLatched = true;
|
|
207
201
|
}
|
|
@@ -246,15 +240,28 @@ export class AnthropicProvider {
|
|
|
246
240
|
fastMode: this.fastModeBetaHeaderLatched,
|
|
247
241
|
toolSearch: hasDeferredTools,
|
|
248
242
|
effort: shouldIncludeEffortBeta(useModel, opts),
|
|
243
|
+
serverFallback: params.fallbacks === 'default',
|
|
249
244
|
}),
|
|
250
245
|
};
|
|
251
246
|
|
|
252
247
|
const MAX_MIDSTREAM_RETRIES = ANTHROPIC_MAX_MIDSTREAM_RETRIES;
|
|
253
248
|
let firstAttemptError = null;
|
|
254
249
|
let firstAttemptClassifier = null;
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
const stallRetryBudget =
|
|
250
|
+
// Shared logical-send window: provider fallback and loop replay consume
|
|
251
|
+
// the same recovery budget.
|
|
252
|
+
const stallRetryBudget = resolveStallRetryBudget(opts);
|
|
253
|
+
const requireTransportRecoveryBudget = (error, controller) => {
|
|
254
|
+
if (stallRetryBudget.allowStallRetry()) return;
|
|
255
|
+
try {
|
|
256
|
+
process.stderr.write(
|
|
257
|
+
`[${this.name}] transport recovery budget exhausted (${STREAM_STALL_RETRY_BUDGET_MS}ms since first failure)\n`,
|
|
258
|
+
);
|
|
259
|
+
} catch {}
|
|
260
|
+
try { controller?.abort?.(error); } catch {}
|
|
261
|
+
throw markProviderRecoveryExhausted(error, {
|
|
262
|
+
owner: `${this.name}-transport-budget`,
|
|
263
|
+
});
|
|
264
|
+
};
|
|
258
265
|
|
|
259
266
|
const buildReturnFromParse = (parseResult) => {
|
|
260
267
|
const usageRaw = parseResult.usage?.raw || null;
|
|
@@ -311,6 +318,9 @@ export class AnthropicProvider {
|
|
|
311
318
|
assistantBlocks: Array.isArray(parseResult.assistantBlocks) && parseResult.assistantBlocks.length
|
|
312
319
|
? parseResult.assistantBlocks
|
|
313
320
|
: undefined,
|
|
321
|
+
...(parseResult.providerMetadata && typeof parseResult.providerMetadata === 'object'
|
|
322
|
+
? { providerMetadata: parseResult.providerMetadata }
|
|
323
|
+
: {}),
|
|
314
324
|
usage: {
|
|
315
325
|
inputTokens: input,
|
|
316
326
|
outputTokens: output,
|
|
@@ -356,6 +366,11 @@ export class AnthropicProvider {
|
|
|
356
366
|
},
|
|
357
367
|
);
|
|
358
368
|
return buildReturnFromParse(normalizeAnthropicNonStreamingResponse(message, useModel));
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (error instanceof AnthropicFallbackTriggeredError || totalSignal?.aborted) throw error;
|
|
371
|
+
throw markProviderRecoveryExhausted(error, {
|
|
372
|
+
owner: `${this.name}-nonstreaming-fallback`,
|
|
373
|
+
});
|
|
359
374
|
} finally {
|
|
360
375
|
lifetime.cleanup();
|
|
361
376
|
}
|
|
@@ -379,6 +394,7 @@ export class AnthropicProvider {
|
|
|
379
394
|
try { streamingError.liveTextEmitted = true; streamingError.unsafeToRetry = true; } catch {}
|
|
380
395
|
throw streamingError;
|
|
381
396
|
}
|
|
397
|
+
requireTransportRecoveryBudget(streamingError, streamController);
|
|
382
398
|
return issueNonStreamingFallback(streamController, streamingError);
|
|
383
399
|
};
|
|
384
400
|
|
|
@@ -469,6 +485,12 @@ export class AnthropicProvider {
|
|
|
469
485
|
model: useModel,
|
|
470
486
|
fallbackModel: opts._fallbackTriggered ? undefined : opts.fallbackModel,
|
|
471
487
|
onRetry: ({ attempt, lastErr, delayMs, delayReason }) => {
|
|
488
|
+
// Long/unknown fast-pool window: replay at
|
|
489
|
+
// standard speed rather than waiting it out.
|
|
490
|
+
if (params?.speed === 'fast'
|
|
491
|
+
&& noteFastModeCapacityError(lastErr, { fast: true }) !== 'retry-fast') {
|
|
492
|
+
delete params.speed;
|
|
493
|
+
}
|
|
472
494
|
const status = Number(lastErr?.httpStatus || lastErr?.status || lastErr?.response?.status || 0);
|
|
473
495
|
if (status === 429) notifyCurrentAnthropicRateLimit(lastErr);
|
|
474
496
|
const delayLabel = Number.isFinite(Number(delayMs))
|
|
@@ -628,17 +650,12 @@ export class AnthropicProvider {
|
|
|
628
650
|
&& !midState.emittedToolCall
|
|
629
651
|
&& !midState.partialToolCall
|
|
630
652
|
&& !midState.emittedThinking) {
|
|
653
|
+
requireTransportRecoveryBudget(err, streamController);
|
|
631
654
|
try { process.stderr.write(`[${this.name}] stream stalled with no exposure — retrying non-streaming\n`); } catch {}
|
|
632
655
|
return await issueNonStreamingFallback(streamController, err);
|
|
633
656
|
}
|
|
634
|
-
if (classifier === 'stream_stalled'
|
|
635
|
-
|
|
636
|
-
process.stderr.write(
|
|
637
|
-
`[${this.name}] stall retry budget exhausted (${STREAM_STALL_RETRY_BUDGET_MS}ms since first stall) — surfacing for fresh-request retry\n`,
|
|
638
|
-
);
|
|
639
|
-
} catch {}
|
|
640
|
-
try { streamController.abort?.(err); } catch {}
|
|
641
|
-
throw err;
|
|
657
|
+
if (classifier === 'stream_stalled') {
|
|
658
|
+
requireTransportRecoveryBudget(err, streamController);
|
|
642
659
|
}
|
|
643
660
|
if (classifier && attemptIndex < MAX_MIDSTREAM_RETRIES) {
|
|
644
661
|
firstAttemptError = err;
|
|
@@ -664,6 +681,12 @@ export class AnthropicProvider {
|
|
|
664
681
|
);
|
|
665
682
|
continue;
|
|
666
683
|
}
|
|
684
|
+
if (classifier && attemptIndex >= MAX_MIDSTREAM_RETRIES) {
|
|
685
|
+
markProviderRecoveryExhausted(err, {
|
|
686
|
+
owner: `${this.name}-midstream`,
|
|
687
|
+
attempts: attemptIndex + 1,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
667
690
|
if (attemptIndex > 0 && firstAttemptError) {
|
|
668
691
|
try { err.midstreamRetries = attemptIndex; } catch {}
|
|
669
692
|
try { err.midstreamClassifier = firstAttemptClassifier; } catch {}
|
|
@@ -86,6 +86,27 @@ function antigravityError(res, text, endpoint) {
|
|
|
86
86
|
return err;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Endpoint failover only makes sense when the HOST is the suspect. A
|
|
90
|
+
// deterministic 4xx — malformed request, revoked/expired auth, permission
|
|
91
|
+
// denial, unknown route — and a quota decision (429) are answers from the
|
|
92
|
+
// account or the request itself: replaying them across every alternate host
|
|
93
|
+
// repeats the identical rejection, hides the real error behind the last host's
|
|
94
|
+
// message, and can duplicate an accepted-but-throttled generation. Only 5xx and
|
|
95
|
+
// transport-level failures (status 0: DNS/connect/TLS/timeout) fail over.
|
|
96
|
+
function antigravityFailoverEligible(err) {
|
|
97
|
+
if (err?.unsafeToRetry === true || err?.liveTextEmitted === true || err?.emittedToolCall === true) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const status = Number(err?.status || err?.httpStatus || 0);
|
|
101
|
+
// Strictly 5xx or transport. status 0 means no HTTP response reached us at
|
|
102
|
+
// all (DNS/connect/TLS/first-byte timeout) — the only genuine "this host is
|
|
103
|
+
// the problem" evidence besides a server-side 5xx. ANY other answer,
|
|
104
|
+
// including a 3xx redirect the host chose to return, is that host's real
|
|
105
|
+
// reply and would be reproduced identically everywhere else.
|
|
106
|
+
if (status >= 500) return true;
|
|
107
|
+
return status === 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
// A signed decimal derived from the first user turn, mirroring the real client.
|
|
90
111
|
function deriveSessionId(contents) {
|
|
91
112
|
const first = contents.find((c) => c?.role === 'user');
|
|
@@ -211,6 +232,9 @@ export class AntigravityOAuthProvider {
|
|
|
211
232
|
const endpoints = this._endpointOrder();
|
|
212
233
|
let lastErr = null;
|
|
213
234
|
let response = null;
|
|
235
|
+
// One forced token refresh per send: a second 401 after a fresh token is
|
|
236
|
+
// a real authorization failure, not a stale bearer.
|
|
237
|
+
let refreshedAuth = false;
|
|
214
238
|
try {
|
|
215
239
|
for (let index = 0; index < endpoints.length; index += 1) {
|
|
216
240
|
const endpoint = endpoints[index];
|
|
@@ -275,10 +299,40 @@ export class AntigravityOAuthProvider {
|
|
|
275
299
|
break;
|
|
276
300
|
} catch (err) {
|
|
277
301
|
lastErr = err;
|
|
302
|
+
const status = Number(err?.status || err?.httpStatus || 0);
|
|
303
|
+
const emitted = err?.unsafeToRetry === true
|
|
304
|
+
|| err?.liveTextEmitted === true
|
|
305
|
+
|| err?.emittedToolCall === true;
|
|
306
|
+
// A typed 401 says THIS access token is no longer accepted —
|
|
307
|
+
// every host would reject it identically. Force one
|
|
308
|
+
// credential refresh and replay the same endpoint instead of
|
|
309
|
+
// failing over with the same dead token (and instead of
|
|
310
|
+
// surfacing a re-login prompt for a token that only needed a
|
|
311
|
+
// refresh). shouldRefresh() alone never covers this: a
|
|
312
|
+
// server-side revocation/rotation happens while the local
|
|
313
|
+
// expiry still looks valid.
|
|
314
|
+
if (status === 401 && !emitted && !refreshedAuth) {
|
|
315
|
+
refreshedAuth = true;
|
|
316
|
+
let refreshed = null;
|
|
317
|
+
try {
|
|
318
|
+
refreshed = await this._ensureAuth({ fetchFn: this._fetch, force: true });
|
|
319
|
+
} catch {
|
|
320
|
+
// Refresh itself failed (revoked / no refresh token):
|
|
321
|
+
// the original 401 is the actionable error.
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
// refreshTokens() preserves project_id/email, so only the
|
|
325
|
+
// bearer changes; the already-serialized body stays valid.
|
|
326
|
+
this._projectId = refreshed.projectId;
|
|
327
|
+
headers.Authorization = `Bearer ${refreshed.accessToken}`;
|
|
328
|
+
textLeakGuard = null;
|
|
329
|
+
process.stderr.write('[antigravity] 401 — refreshed credentials and retrying once\n');
|
|
330
|
+
index -= 1;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
278
333
|
// Anything already streamed to the user must not be replayed on
|
|
279
334
|
// another endpoint, and a terminal decision is not transport luck.
|
|
280
|
-
if (isLast || err
|
|
281
|
-
|| err?.emittedToolCall === true) {
|
|
335
|
+
if (isLast || !antigravityFailoverEligible(err)) {
|
|
282
336
|
throw err;
|
|
283
337
|
}
|
|
284
338
|
process.stderr.write(`[antigravity] ${endpoint} failed (${err?.message || err}); trying next endpoint\n`);
|
|
@@ -10,8 +10,30 @@ const LOGIN_URL = 'https://cursor.com/loginDeepControl';
|
|
|
10
10
|
const POLL_URL = 'https://api2.cursor.sh/auth/poll';
|
|
11
11
|
const REFRESH_URL = 'https://api2.cursor.sh/auth/exchange_user_api_key';
|
|
12
12
|
const REFRESH_SKEW_MS = 5 * 60_000;
|
|
13
|
+
// Upper bound for the SHARED token exchange. It replaces the first caller's
|
|
14
|
+
// abort signal, which must never govern work every other waiter depends on.
|
|
15
|
+
const REFRESH_TIMEOUT_MS = 30_000;
|
|
13
16
|
let refreshInFlight = null;
|
|
14
17
|
|
|
18
|
+
// Wait on the shared refresh while still honouring THIS caller's abort signal.
|
|
19
|
+
// Aborting only ends this caller's wait; the exchange keeps running for the
|
|
20
|
+
// remaining waiters and still publishes its result.
|
|
21
|
+
function awaitSharedRefresh(promise, signal) {
|
|
22
|
+
if (!(signal instanceof AbortSignal)) return promise;
|
|
23
|
+
const abortError = () => (signal.reason instanceof Error
|
|
24
|
+
? signal.reason
|
|
25
|
+
: new Error('Cursor token refresh wait aborted'));
|
|
26
|
+
if (signal.aborted) return Promise.reject(abortError());
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const onAbort = () => reject(abortError());
|
|
29
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
30
|
+
promise.then(
|
|
31
|
+
(value) => { signal.removeEventListener('abort', onAbort); resolve(value); },
|
|
32
|
+
(err) => { signal.removeEventListener('abort', onAbort); reject(err); },
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
function credentialsPath() {
|
|
16
38
|
const bound = boundProviderAuthPath('cursor-oauth');
|
|
17
39
|
if (bound) return resolve(bound);
|
|
@@ -231,7 +253,15 @@ export async function resolveCursorOAuthAccessToken({ forceRefresh = false, fetc
|
|
|
231
253
|
if (!latest.refresh_token) {
|
|
232
254
|
throw new Error('Cursor OAuth refresh token is unavailable. Open /providers in Mixdog to sign in again.');
|
|
233
255
|
}
|
|
234
|
-
|
|
256
|
+
// Deliberately NOT the caller's signal: this exchange is shared by
|
|
257
|
+
// every concurrent waiter, so letting the first caller's abort
|
|
258
|
+
// (turn cancel / session close) kill it would fail the refresh for
|
|
259
|
+
// all of them and leave the process without a usable token. Its own
|
|
260
|
+
// timeout bounds the request instead.
|
|
261
|
+
const next = await exchangeCursorToken(latest.refresh_token, {
|
|
262
|
+
fetchFn,
|
|
263
|
+
signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS),
|
|
264
|
+
});
|
|
235
265
|
if (!process.env.CURSOR_ACCESS_TOKEN) {
|
|
236
266
|
saveCredentials(next);
|
|
237
267
|
}
|
|
@@ -242,7 +272,9 @@ export async function resolveCursorOAuthAccessToken({ forceRefresh = false, fetc
|
|
|
242
272
|
secret: true,
|
|
243
273
|
})
|
|
244
274
|
.finally(() => { refreshInFlight = null; });
|
|
275
|
+
// Every waiter may abandon its wait; keep the shared rejection observed.
|
|
276
|
+
refreshInFlight.catch(() => {});
|
|
245
277
|
}
|
|
246
|
-
tokens = await refreshInFlight;
|
|
278
|
+
tokens = await awaitSharedRefresh(refreshInFlight, signal);
|
|
247
279
|
return tokens.access_token;
|
|
248
280
|
}
|
|
@@ -117,7 +117,12 @@ function openCursorStream({ accessToken, path = RUN_PATH, url = API_URL }) {
|
|
|
117
117
|
get alive() { return !closed; },
|
|
118
118
|
write(bytes) {
|
|
119
119
|
if (closed) return;
|
|
120
|
-
resetTimeout()
|
|
120
|
+
// Deliberately NOT resetTimeout(): frames written here (client
|
|
121
|
+
// heartbeat every 5s, tool results) are OUR traffic and say nothing
|
|
122
|
+
// about the server still being alive. Refreshing the deadline on
|
|
123
|
+
// every write let a silent server hold the connection open
|
|
124
|
+
// indefinitely, defeating the 120s silence bound this transport
|
|
125
|
+
// owns. Only 'response'/'data' from the server re-arm it.
|
|
121
126
|
request.write(bytes);
|
|
122
127
|
},
|
|
123
128
|
close,
|
|
@@ -383,6 +388,56 @@ function storeActiveRun(key, active) {
|
|
|
383
388
|
activeRuns.set(key, active);
|
|
384
389
|
}
|
|
385
390
|
|
|
391
|
+
// Tear down one stored run: drop it from the registry, stop its 5s heartbeat
|
|
392
|
+
// interval, and close the Cursor bridge it was holding open.
|
|
393
|
+
function closeActiveRun(key, active, error) {
|
|
394
|
+
if (!forgetActiveRun(key, active)) return false;
|
|
395
|
+
clearInterval(active.heartbeat);
|
|
396
|
+
try { active.bridge.close(error); } catch { /* already closing */ }
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// A pending tool batch deliberately outlives its HTTP response: the bridge and
|
|
401
|
+
// heartbeat stay up so the next request can resume the same Cursor run. That
|
|
402
|
+
// connection belongs to the session that opened it, so a session close / turn
|
|
403
|
+
// abort must reclaim it instead of leaving it connected until MEMORY_TTL_MS
|
|
404
|
+
// (30 minutes) expires.
|
|
405
|
+
export function closeCursorRunsForSession(sessionId, reason = 'session_closed') {
|
|
406
|
+
const scope = String(sessionId || '').trim();
|
|
407
|
+
if (!scope) return 0;
|
|
408
|
+
let closed = 0;
|
|
409
|
+
for (const [key, active] of [...activeRuns]) {
|
|
410
|
+
if (String(active?.sessionId || '') !== scope) continue;
|
|
411
|
+
if (closeActiveRun(key, active, new Error(`Cursor run closed (${reason})`))) closed += 1;
|
|
412
|
+
}
|
|
413
|
+
return closed;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Process-wide drain (shutdown / exit): no run may keep the daemon's Cursor
|
|
417
|
+
// sockets and heartbeat intervals alive past teardown.
|
|
418
|
+
export function drainCursorRuns(reason = 'shutdown') {
|
|
419
|
+
let closed = 0;
|
|
420
|
+
for (const [key, active] of [...activeRuns]) {
|
|
421
|
+
if (closeActiveRun(key, active, new Error(`Cursor run drained (${reason})`))) closed += 1;
|
|
422
|
+
}
|
|
423
|
+
return closed;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Session-close / drain hooks. Both globals are shared with the openai WS pool,
|
|
427
|
+
// so chain the previously registered handler instead of overwriting it: whoever
|
|
428
|
+
// loads first stays reachable, in either import order.
|
|
429
|
+
const _priorCursorSessionCloseHook = globalThis.__mixdogCloseProviderConnectionsForSession;
|
|
430
|
+
globalThis.__mixdogCloseProviderConnectionsForSession = (sessionId, reason) => {
|
|
431
|
+
try { _priorCursorSessionCloseHook?.(sessionId, reason); } finally {
|
|
432
|
+
closeCursorRunsForSession(sessionId, reason);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const _priorCursorDrainHook = globalThis.__mixdogDrainProviderConnections;
|
|
436
|
+
globalThis.__mixdogDrainProviderConnections = (reason) => {
|
|
437
|
+
try { _priorCursorDrainHook?.(reason); } finally { drainCursorRuns(reason); }
|
|
438
|
+
};
|
|
439
|
+
process.on('exit', () => { drainCursorRuns('process-exit'); });
|
|
440
|
+
|
|
386
441
|
function conversationKey(messages, sessionId = '') {
|
|
387
442
|
const scope = String(sessionId || '').trim();
|
|
388
443
|
if (scope) {
|
|
@@ -889,6 +944,9 @@ function createStreamResponse({
|
|
|
889
944
|
cloudRule,
|
|
890
945
|
model,
|
|
891
946
|
key,
|
|
947
|
+
// Owning session, carried into the stored run so a session close can find
|
|
948
|
+
// and tear down the pending batch's bridge/heartbeat.
|
|
949
|
+
sessionId = '',
|
|
892
950
|
sawTurnEnded = false,
|
|
893
951
|
}) {
|
|
894
952
|
const id = `chatcmpl-${crypto.randomUUID().replaceAll('-', '').slice(0, 28)}`;
|
|
@@ -944,6 +1002,7 @@ function createStreamResponse({
|
|
|
944
1002
|
conversation,
|
|
945
1003
|
tools,
|
|
946
1004
|
cloudRule,
|
|
1005
|
+
sessionId,
|
|
947
1006
|
pending: state.pending,
|
|
948
1007
|
sawTurnEnded: state.sawTurnEnded,
|
|
949
1008
|
});
|
|
@@ -1183,6 +1242,7 @@ export async function handleChatCompletion(body, accessToken) {
|
|
|
1183
1242
|
cloudRule: parsed.systems.join('\n\n') || undefined,
|
|
1184
1243
|
model,
|
|
1185
1244
|
key,
|
|
1245
|
+
sessionId,
|
|
1186
1246
|
});
|
|
1187
1247
|
}
|
|
1188
1248
|
|
|
@@ -447,7 +447,6 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
447
447
|
}
|
|
448
448
|
const { done, value } = chunk;
|
|
449
449
|
if (done) break;
|
|
450
|
-
resetIdleTimer();
|
|
451
450
|
buffer += decoder.decode(value, { stream: true });
|
|
452
451
|
const payloads = [];
|
|
453
452
|
let lineEnd;
|
|
@@ -483,6 +482,12 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
483
482
|
relayedText += relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
|
|
484
483
|
}
|
|
485
484
|
}
|
|
485
|
+
// Re-arm on SEMANTIC progress only. A decoded SSE payload is real
|
|
486
|
+
// generation output; raw byte chunks (a partial data: line, blank
|
|
487
|
+
// keepalive lines, proxy padding) are not. Resetting on every read
|
|
488
|
+
// let a stalled generation that still dribbles bytes hold the idle
|
|
489
|
+
// watchdog off indefinitely.
|
|
490
|
+
if (parsedChunks.length) resetIdleTimer();
|
|
486
491
|
}
|
|
487
492
|
if (buffer.trim()) {
|
|
488
493
|
const line = buffer.trim().replace(/\r$/, '');
|