@yansigit/opencodex 2.33.1 → 2.35.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 +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -3
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +40 -3
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +9 -9
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-shell.ts +103 -6
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +74 -39
- package/src/adapters/cursor/request-builder.ts +32 -10
- package/src/adapters/cursor/tool-definitions.ts +13 -10
- package/src/adapters/cursor/tool-result-normalize.ts +35 -3
- package/src/adapters/cursor/types.ts +16 -4
- package/src/adapters/cursor.ts +97 -70
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +156 -65
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +334 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -37
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +229 -153
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +54 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +19 -20
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +73 -14
- package/src/providers/registry.ts +160 -68
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +23 -0
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +152 -80
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +46 -20
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +571 -137
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +15 -4
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +77 -0
- package/src/types/request.ts +16 -10
- package/src/types/tools.ts +20 -25
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +17 -7
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-CIDo4y4k.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
- package/src/server/aistudio-ws-hub.ts +0 -295
|
@@ -29,6 +29,31 @@ function isNodeReplOrComputerUseTool(toolName?: string, toolNamespace?: string):
|
|
|
29
29
|
return lower.startsWith("mcp__node_repl") || lower.startsWith("mcp__computer_use");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Codex exec / shell-bridge tool names (flat and MCP-prefixed display aliases). An empty result
|
|
34
|
+
* here is almost always a code-mode cell that never called text()/notify() — the cursor model
|
|
35
|
+
* reads the blank [tool_result], concludes prior results were lost, and spirals into
|
|
36
|
+
* re-orientation retries (devlog 260826_cursor_responses_gap, live subagent transcripts).
|
|
37
|
+
*/
|
|
38
|
+
function isCodexExecBridgeTool(toolName?: string, toolNamespace?: string): boolean {
|
|
39
|
+
if (toolNamespace && toolNamespace.includes("opencodex-responses")) return true;
|
|
40
|
+
if (!toolName) return false;
|
|
41
|
+
const lower = toolName.toLowerCase();
|
|
42
|
+
return (
|
|
43
|
+
lower === "exec"
|
|
44
|
+
|| lower === "exec_command"
|
|
45
|
+
|| lower === "shell_command"
|
|
46
|
+
// Codex CLI/desktop native tool names: the multi-round "이전 출력이 비어 있어 처음부터"
|
|
47
|
+
// restart loop reproduced via codex exec because `shell` was not in this set
|
|
48
|
+
// (devlog 260826 gap-8 QA round 2).
|
|
49
|
+
|| lower === "shell"
|
|
50
|
+
|| lower === "local_shell"
|
|
51
|
+
|| lower === "container.exec"
|
|
52
|
+
|| lower.startsWith("mcp_opencodex-responses_")
|
|
53
|
+
|| lower.startsWith("mcp__opencodex-responses__")
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
/** Failure states the Computer Use / node_repl runtime reports as PLAIN TEXT inside a non-error result. */
|
|
33
58
|
const RUNTIME_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string }> = [
|
|
34
59
|
{
|
|
@@ -44,9 +69,9 @@ const RUNTIME_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string
|
|
|
44
69
|
guidance: "The node_repl session keeps earlier declarations; rename the variable or use var/reassignment instead of redeclaring.",
|
|
45
70
|
},
|
|
46
71
|
{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
marker: "unsupported import in exec",
|
|
73
|
+
guidance: "Imports are not available in this exec context; use the injected globals instead.",
|
|
74
|
+
},
|
|
50
75
|
{
|
|
51
76
|
marker: "require is not defined",
|
|
52
77
|
guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
|
|
@@ -96,6 +121,13 @@ export function normalizeCursorToolResultText(
|
|
|
96
121
|
changed: true,
|
|
97
122
|
};
|
|
98
123
|
}
|
|
124
|
+
if (isCodexExecBridgeTool(options.toolName, options.toolNamespace) && EMPTY_EXEC_OUTPUT_REGEX.test(text.trim())) {
|
|
125
|
+
return {
|
|
126
|
+
text: "[empty output: the exec cell completed but emitted nothing. This is NOT lost context and NOT a blocked tool — in code mode call text(...) or notify(...) on any value you need to see (a bare await tools.exec_command(...) is not echoed automatically); in shell mode the command simply printed nothing. Do not re-run the same call expecting different output.]",
|
|
127
|
+
isError: false,
|
|
128
|
+
changed: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
99
131
|
for (const { marker, guidance } of RUNTIME_FAILURE_GUIDANCE) {
|
|
100
132
|
if (text.includes(marker) && !text.includes("[recovery:") && !text.includes(guidance)) {
|
|
101
133
|
return { text: `${text}\n[recovery: ${guidance}]`, isError: true, changed: true };
|
|
@@ -15,6 +15,19 @@ export interface CursorRunRequest {
|
|
|
15
15
|
requestedModelParameters?: readonly CursorRequestedModelParameter[];
|
|
16
16
|
/** Cursor Router optimization parameter; valid only while modelId is the `default` wire model. */
|
|
17
17
|
routingLevel?: CursorRoutingLevel;
|
|
18
|
+
/**
|
|
19
|
+
* Cursor Max Mode (ultra/big-context). Set from a synthetic `-1m` picker variant; the wire
|
|
20
|
+
* keeps the original model id and raises RequestedModel.maxMode + ModelDetails.maxMode
|
|
21
|
+
* (both fields — missing either can invalid_argument upstream). Devlog 260826 070.
|
|
22
|
+
*/
|
|
23
|
+
maxMode?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Bare API callers (no caller tools, no Codex thread identity) pay a ~10-15K input-token
|
|
26
|
+
* preamble because an absent AgentRunRequest.mcp_tools field makes Cursor inject its default
|
|
27
|
+
* native tool catalog. When true, an explicitly empty McpTools wrapper is serialized instead,
|
|
28
|
+
* suppressing that default. Codex-identified sessions keep the absent-field behavior.
|
|
29
|
+
*/
|
|
30
|
+
suppressDefaultCursorToolCatalog?: boolean;
|
|
18
31
|
/**
|
|
19
32
|
* Corrective active-turn text for the single envelope-echo retry (devlog 260826 gap-10).
|
|
20
33
|
* When set on an external tool-result continuation, buildPreparedCursorRunRequest uses it as
|
|
@@ -32,10 +45,9 @@ export interface CursorRunRequest {
|
|
|
32
45
|
* hydration). History stays text-only. data: URLs only in this slice.
|
|
33
46
|
*/
|
|
34
47
|
selectedImages?: readonly ResolvedCursorImage[];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
parallelToolCalls?: boolean;
|
|
48
|
+
tools?: OcxTool[];
|
|
49
|
+
toolChoice?: OcxRequestOptions["toolChoice"];
|
|
50
|
+
parallelToolCalls?: boolean;
|
|
39
51
|
/**
|
|
40
52
|
* Clear provider-private context-usage carry-forward before this run. Used when Codex starts a
|
|
41
53
|
* newly observed compacted context epoch, so pre-compaction totals are not over-reported while
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -3,18 +3,13 @@ import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types
|
|
|
3
3
|
import type { ProviderAdapter } from "./base";
|
|
4
4
|
import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
|
|
5
5
|
import { cursorExecDeniedMessage, cursorRequestDeclaresFullAccess } from "./cursor/exec-policy";
|
|
6
|
-
import {
|
|
7
|
-
isCursorBenignCancelError,
|
|
8
|
-
isCursorInvalidArgumentError,
|
|
9
|
-
isCursorOverflowRemintCandidate,
|
|
10
|
-
safeCursorErrorMessage,
|
|
11
|
-
type CursorSizeContext,
|
|
12
|
-
} from "./cursor/cursor-errors";
|
|
6
|
+
import { isCursorBenignCancelError, isCursorInvalidArgumentError, isCursorOverflowRemintCandidate, safeCursorErrorMessage, type CursorSizeContext } from "./cursor/cursor-errors";
|
|
13
7
|
import { cursorCheckpointModelAffinityId, inferCursorContextWindow, isCursorExternalWireModel } from "./cursor/discovery";
|
|
14
8
|
import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
|
|
15
9
|
import { mapCursorServerMessage } from "./cursor/message-mapper";
|
|
16
10
|
import {
|
|
17
11
|
createCursorRequest,
|
|
12
|
+
cursorClientThreadOwner,
|
|
18
13
|
cursorCoveredPrefixDigest,
|
|
19
14
|
cursorInstructionDigest,
|
|
20
15
|
} from "./cursor/request-builder";
|
|
@@ -176,15 +171,36 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
176
171
|
let completedNormally = false;
|
|
177
172
|
let lastTransport: { captured?: Uint8Array } | undefined;
|
|
178
173
|
let emittedClientTool = false;
|
|
174
|
+
// Ordering proof for tool-suspended checkpoints: true only when the newest captured
|
|
175
|
+
// checkpoint bytes arrived AFTER the turn emitted a client tool call, i.e. upstream
|
|
176
|
+
// serialized its suspended-on-tool-call state. Only that snapshot can safely resume
|
|
177
|
+
// with the covered-prefix + trailing-toolResult path (devlog 260826 050).
|
|
178
|
+
let capturedAfterClientTool = false;
|
|
179
179
|
|
|
180
180
|
const commitCapturedCheckpoint = (activeRequest: ReturnType<typeof createCursorRequest>): void => {
|
|
181
|
+
const toolSuspendedCommit =
|
|
182
|
+
emittedClientTool
|
|
183
|
+
&& capturedAfterClientTool
|
|
184
|
+
&& isCursorExternalWireModel(activeRequest.modelId);
|
|
181
185
|
if (
|
|
182
186
|
replayUnsafe
|
|
183
|
-
|| emittedClientTool
|
|
187
|
+
|| (emittedClientTool && !toolSuspendedCommit)
|
|
184
188
|
|| activeRequest.contextUsageStoreCheckpoints === false
|
|
185
189
|
|| !lastTransport?.captured
|
|
186
190
|
|| lastTransport.captured.byteLength === 0
|
|
187
|
-
)
|
|
191
|
+
) {
|
|
192
|
+
// Refusal diagnostics (devlog 260826 050/080): name the exact guard so a live
|
|
193
|
+
// missing_ref chain can be attributed without instrumented rebuilds.
|
|
194
|
+
debugProviderDiagnostic("cursor", "checkpoint-commit-refused", {
|
|
195
|
+
replayUnsafe,
|
|
196
|
+
emittedClientTool,
|
|
197
|
+
capturedAfterClientTool,
|
|
198
|
+
externalModel: isCursorExternalWireModel(activeRequest.modelId),
|
|
199
|
+
storeCheckpoints: activeRequest.contextUsageStoreCheckpoints !== false,
|
|
200
|
+
capturedBytes: lastTransport?.captured?.byteLength ?? 0,
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
188
204
|
const previousRef = _parsed._providerContinuation?.cursor?.checkpointRef;
|
|
189
205
|
const coveredMessageCount = _parsed.context.messages.length;
|
|
190
206
|
const checkpointRef = commitCursorCheckpoint({
|
|
@@ -203,7 +219,9 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
203
219
|
cursor: {
|
|
204
220
|
...(_parsed._providerContinuation?.cursor ?? {}),
|
|
205
221
|
conversationId: activeRequest.conversationId,
|
|
206
|
-
|
|
222
|
+
// A tool-suspended checkpoint is only usable by the immediate trailing-toolResult
|
|
223
|
+
// continuation; the request-builder guard keys on checkpointUsable=false for that.
|
|
224
|
+
checkpointUsable: !toolSuspendedCommit,
|
|
207
225
|
checkpointRef,
|
|
208
226
|
},
|
|
209
227
|
};
|
|
@@ -213,6 +231,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
213
231
|
checkpointRefHash: cursorCheckpointRefHash(checkpointRef),
|
|
214
232
|
checkpointBytes: lastTransport.captured.byteLength,
|
|
215
233
|
wireModel: activeRequest.modelId,
|
|
234
|
+
...(toolSuspendedCommit ? { toolSuspended: true } : {}),
|
|
216
235
|
});
|
|
217
236
|
};
|
|
218
237
|
|
|
@@ -225,6 +244,14 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
225
244
|
|| activeRequest.modelId.includes("kimi-k3")
|
|
226
245
|
|| activeRequest.modelId.includes("opus-4-8");
|
|
227
246
|
const heartbeatOnlyMs = isHeavyReasoning ? 300_000 : 180_000;
|
|
247
|
+
// Envelope echo quarantine (devlog 260826 gap-10): external full-replay continuations
|
|
248
|
+
// whose trailing input is a tool result sometimes ECHO the replayed "[Tool Result]"
|
|
249
|
+
// envelope as assistant text (kimi-k3 ~30-40% of multi-round probes). Hold the first
|
|
250
|
+
// text deltas until they provably diverge from the markers; a completed marker turns
|
|
251
|
+
// the turn into a retryable semantic failure BEFORE any client-visible delta escapes.
|
|
252
|
+
// Armed for ANY external turn whose replayed history contains a tool result — echo
|
|
253
|
+
// priming was observed live on user-action rounds too (the envelope lives in the
|
|
254
|
+
// flattened history regardless of which role ends the input).
|
|
228
255
|
const armEchoSniffer =
|
|
229
256
|
isCursorExternalWireModel(activeRequest.modelId)
|
|
230
257
|
&& (_parsed.context.messages ?? []).some(message => message.role === "toolResult");
|
|
@@ -271,14 +298,17 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
271
298
|
if (message.type === "done") completedNormally = true;
|
|
272
299
|
if (message.type === "tool_call_end") emittedClientTool = true;
|
|
273
300
|
const captured = capturedCursorCheckpointBytes(activeTransport);
|
|
274
|
-
if (captured)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
301
|
+
if (captured) {
|
|
302
|
+
if (captured !== lastTransport?.captured) capturedAfterClientTool = emittedClientTool;
|
|
303
|
+
lastTransport = { captured };
|
|
304
|
+
}
|
|
305
|
+
const events = mapCursorServerMessage(message, {
|
|
306
|
+
kv,
|
|
307
|
+
writeClient: clientMessage => {
|
|
308
|
+
void activeTransport.writeClient(clientMessage);
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
for (const event of events) {
|
|
282
312
|
if (!guardsSettled()) {
|
|
283
313
|
if (event.type === "text_delta") {
|
|
284
314
|
guardHeld.push(event);
|
|
@@ -354,9 +384,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
354
384
|
const next = createCursorRequest(_parsed, { forceFreshConversation: true });
|
|
355
385
|
rekeyContextUsage(failedConversationId, next.conversationId);
|
|
356
386
|
_parsed._cursorConversationId = next.conversationId;
|
|
357
|
-
|
|
387
|
+
const threadOwner = cursorClientThreadOwner(_parsed);
|
|
388
|
+
if (threadOwner && _parsed._cursorIsolateConversation !== true) {
|
|
358
389
|
rememberCursorThreadConversation(
|
|
359
|
-
|
|
390
|
+
threadOwner,
|
|
360
391
|
next.conversationId,
|
|
361
392
|
_parsed._cursorIdentityScope,
|
|
362
393
|
);
|
|
@@ -369,50 +400,51 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
369
400
|
await runOnce(request);
|
|
370
401
|
break;
|
|
371
402
|
} catch (err) {
|
|
372
|
-
|
|
403
|
+
const outputGuardRetryText =
|
|
404
|
+
err instanceof CursorToolResultEchoError
|
|
405
|
+
? CURSOR_ECHO_RETRY_CONTINUATION_TEXT
|
|
406
|
+
: err instanceof CursorRoutingCommentaryError
|
|
407
|
+
? CURSOR_ROUTING_COMMENTARY_RETRY_TEXT
|
|
408
|
+
: undefined;
|
|
409
|
+
// One-shot corrective retry for guarded external output (devlog 260826 gap-10/11).
|
|
410
|
+
// The quarantine guarantees no client-visible delta escaped, so a fresh-conversation
|
|
411
|
+
// retry is safe. A second rejection propagates as an error rather than looping.
|
|
412
|
+
if (
|
|
413
|
+
outputGuardRetryText
|
|
414
|
+
&& !emittedOutput
|
|
415
|
+
&& !replayUnsafe
|
|
416
|
+
&& !incoming.abortSignal?.aborted
|
|
417
|
+
) {
|
|
418
|
+
debugProviderDiagnostic(
|
|
419
|
+
"cursor",
|
|
373
420
|
err instanceof CursorToolResultEchoError
|
|
374
|
-
?
|
|
375
|
-
:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
421
|
+
? "envelope-echo-retry"
|
|
422
|
+
: "routing-commentary-retry",
|
|
423
|
+
{
|
|
424
|
+
wireModel: request.modelId,
|
|
425
|
+
conversationHash: request.conversationId.slice(0, 16),
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
const echoedConversationId = request.conversationId;
|
|
429
|
+
lastTransport = undefined;
|
|
430
|
+
_parsed._cursorConversationId = undefined;
|
|
431
|
+
request = {
|
|
432
|
+
...createCursorRequest(_parsed, { forceFreshConversation: true }),
|
|
433
|
+
echoRetryContinuationText: outputGuardRetryText,
|
|
434
|
+
};
|
|
435
|
+
rekeyContextUsage(echoedConversationId, request.conversationId);
|
|
436
|
+
_parsed._cursorConversationId = request.conversationId;
|
|
437
|
+
const echoThreadOwner = cursorClientThreadOwner(_parsed);
|
|
438
|
+
if (echoThreadOwner && _parsed._cursorIsolateConversation !== true) {
|
|
439
|
+
rememberCursorThreadConversation(
|
|
440
|
+
echoThreadOwner,
|
|
441
|
+
request.conversationId,
|
|
442
|
+
_parsed._cursorIdentityScope,
|
|
396
443
|
);
|
|
397
|
-
const echoedConversationId = request.conversationId;
|
|
398
|
-
lastTransport = undefined;
|
|
399
|
-
_parsed._cursorConversationId = undefined;
|
|
400
|
-
request = {
|
|
401
|
-
...createCursorRequest(_parsed, { forceFreshConversation: true }),
|
|
402
|
-
echoRetryContinuationText: outputGuardRetryText,
|
|
403
|
-
};
|
|
404
|
-
rekeyContextUsage(echoedConversationId, request.conversationId);
|
|
405
|
-
_parsed._cursorConversationId = request.conversationId;
|
|
406
|
-
if (_parsed._clientThreadId && _parsed._cursorIsolateConversation !== true) {
|
|
407
|
-
rememberCursorThreadConversation(
|
|
408
|
-
_parsed._clientThreadId,
|
|
409
|
-
request.conversationId,
|
|
410
|
-
_parsed._cursorIdentityScope,
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
await runOnce(request);
|
|
414
|
-
break;
|
|
415
444
|
}
|
|
445
|
+
await runOnce(request);
|
|
446
|
+
break;
|
|
447
|
+
} else {
|
|
416
448
|
const overflowRemintSafe =
|
|
417
449
|
!lastRawIsToolResult
|
|
418
450
|
&& !emittedOutput
|
|
@@ -422,25 +454,19 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
422
454
|
_parsed,
|
|
423
455
|
overflowRemintBaseId ?? request.conversationId,
|
|
424
456
|
);
|
|
425
|
-
|
|
426
457
|
if (
|
|
427
458
|
overflowScopeKey
|
|
428
459
|
&& overflowRemintSafe
|
|
429
460
|
&& isCursorOverflowRemintCandidate(err, requestSizeContext)
|
|
430
461
|
) {
|
|
431
|
-
if (shouldSkipCursorOverflowRemint(overflowScopeKey))
|
|
432
|
-
throw err;
|
|
433
|
-
}
|
|
462
|
+
if (shouldSkipCursorOverflowRemint(overflowScopeKey)) throw err;
|
|
434
463
|
if (shouldSurfaceCursorOverflowFirst(overflowScopeKey)) {
|
|
435
464
|
markCursorOverflowSurfaced(overflowScopeKey);
|
|
436
465
|
throw err;
|
|
437
466
|
}
|
|
438
|
-
if (!recordCursorOverflowRemint(overflowScopeKey))
|
|
439
|
-
throw err;
|
|
440
|
-
}
|
|
441
|
-
const failedConversationId = request.conversationId;
|
|
467
|
+
if (!recordCursorOverflowRemint(overflowScopeKey)) throw err;
|
|
442
468
|
if (inheritedCheckpointRef) invalidateCursorCheckpoint(inheritedCheckpointRef);
|
|
443
|
-
request = remintConversationId(
|
|
469
|
+
request = remintConversationId(request.conversationId);
|
|
444
470
|
continue;
|
|
445
471
|
}
|
|
446
472
|
|
|
@@ -462,6 +488,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
462
488
|
break;
|
|
463
489
|
}
|
|
464
490
|
}
|
|
491
|
+
}
|
|
465
492
|
if (
|
|
466
493
|
request.checkpointInvalidationReason
|
|
467
494
|
&& request.checkpointInvalidationReason !== "missing_ref"
|
|
@@ -3,6 +3,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:f
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { atomicWriteFileAsync, getConfigDir, type AtomicWriteAsyncTestSeam } from "../config";
|
|
5
5
|
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
6
|
+
import { ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL } from "./google-antigravity-wire";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Google-family thoughtSignature reasoning-replay cache.
|
|
@@ -19,6 +20,7 @@ import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
|
19
20
|
|
|
20
21
|
interface ReplayCall {
|
|
21
22
|
signature: string;
|
|
23
|
+
signatures?: string[];
|
|
22
24
|
sizeBytes: number;
|
|
23
25
|
touchedAtMs: number;
|
|
24
26
|
}
|
|
@@ -34,6 +36,7 @@ interface ReplayEntry {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
const MIN_SIGNATURE_LEN = 16;
|
|
39
|
+
const MAX_SIGNATURES_PER_CALL = 32;
|
|
37
40
|
const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h
|
|
38
41
|
export const ANTIGRAVITY_REPLAY_MAX_ENTRIES = 10_240;
|
|
39
42
|
const REPLAY_EVICT_BATCH = 128;
|
|
@@ -103,16 +106,38 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
|
|
|
103
106
|
for (const pair of rec.byCall) {
|
|
104
107
|
if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string") continue;
|
|
105
108
|
if (!/^[0-9a-f]{64}$/.test(pair[0])) continue;
|
|
106
|
-
const call = pair[1] as { signature?: unknown; touchedAtMs?: unknown } | null;
|
|
109
|
+
const call = pair[1] as { signature?: unknown; signatures?: unknown; touchedAtMs?: unknown } | null;
|
|
107
110
|
if (!call || typeof call !== "object" || Array.isArray(call)) continue;
|
|
108
111
|
if (typeof call.signature !== "string" || call.signature.length < MIN_SIGNATURE_LEN) continue;
|
|
109
112
|
if (typeof call.touchedAtMs !== "number" || !Number.isFinite(call.touchedAtMs)) continue;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
let sigs: string[] = [];
|
|
114
|
+
if (Array.isArray(call.signatures)) {
|
|
115
|
+
for (const s of call.signatures) {
|
|
116
|
+
if (typeof s === "string" && s.length >= MIN_SIGNATURE_LEN) {
|
|
117
|
+
sigs.push(s);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (sigs.length === 0) {
|
|
122
|
+
sigs = [call.signature];
|
|
123
|
+
} else if (!sigs.includes(call.signature)) {
|
|
124
|
+
sigs.push(call.signature);
|
|
125
|
+
}
|
|
126
|
+
if (sigs.length > MAX_SIGNATURES_PER_CALL) {
|
|
127
|
+
sigs = sigs.slice(-MAX_SIGNATURES_PER_CALL);
|
|
128
|
+
}
|
|
129
|
+
let totalSigBytes = 0;
|
|
130
|
+
let anySigOversized = false;
|
|
131
|
+
for (const s of sigs) {
|
|
132
|
+
const sb = utf8.encode(s).byteLength;
|
|
133
|
+
if (sb > replayLimits.maxSignatureBytes) {
|
|
134
|
+
anySigOversized = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
totalSigBytes += sb;
|
|
138
|
+
}
|
|
139
|
+
if (anySigOversized) continue;
|
|
140
|
+
const callBytes = utf8.encode(pair[0]).byteLength + totalSigBytes;
|
|
116
141
|
if (callBytes > replayLimits.maxBytesPerSession) continue;
|
|
117
142
|
// A duplicated call key would overstate entry.bytes (the map keeps only the
|
|
118
143
|
// last value) and could evict valid sessions; keep the first occurrence.
|
|
@@ -122,6 +147,7 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
|
|
|
122
147
|
}
|
|
123
148
|
byCall.set(pair[0], {
|
|
124
149
|
signature: call.signature,
|
|
150
|
+
signatures: sigs,
|
|
125
151
|
sizeBytes: callBytes,
|
|
126
152
|
touchedAtMs: call.touchedAtMs,
|
|
127
153
|
});
|
|
@@ -226,7 +252,11 @@ async function persistReplaySnapshotNow(): Promise<void> {
|
|
|
226
252
|
for (const [key, entry] of [...replayCache].sort((a, b) => b[1].lastActiveAtMs - a[1].lastActiveAtMs)) {
|
|
227
253
|
const byCall = [...entry.byCall].map(([callKey, call]) => [
|
|
228
254
|
callKey,
|
|
229
|
-
{
|
|
255
|
+
{
|
|
256
|
+
signature: call.signature,
|
|
257
|
+
...(call.signatures && call.signatures.length > 1 ? { signatures: call.signatures } : {}),
|
|
258
|
+
touchedAtMs: call.touchedAtMs,
|
|
259
|
+
},
|
|
230
260
|
]);
|
|
231
261
|
const persistEntry: [string, unknown] = [key, {
|
|
232
262
|
byCall,
|
|
@@ -483,10 +513,10 @@ export function antigravityReplaySessionKeysForTests(): string[] {
|
|
|
483
513
|
|
|
484
514
|
function extractSignature(part: Record<string, unknown>): string | undefined {
|
|
485
515
|
const direct = part.thoughtSignature ?? part.thought_signature;
|
|
486
|
-
if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN) return direct;
|
|
516
|
+
if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN && direct !== ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL) return direct;
|
|
487
517
|
const extra = part.extra_content as { google?: { thought_signature?: unknown } } | undefined;
|
|
488
518
|
const nested = extra?.google?.thought_signature;
|
|
489
|
-
if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN) return nested;
|
|
519
|
+
if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN && nested !== ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL) return nested;
|
|
490
520
|
return undefined;
|
|
491
521
|
}
|
|
492
522
|
|
|
@@ -644,10 +674,37 @@ export function observeAntigravityReplay(
|
|
|
644
674
|
const ck = functionCallKey(fc.name, fc.args);
|
|
645
675
|
if (!ck) continue; // only function-call signatures are replayable by identity
|
|
646
676
|
const signatureBytes = utf8.encode(callSig).byteLength;
|
|
647
|
-
|
|
648
|
-
|
|
677
|
+
if (signatureBytes > replayLimits.maxSignatureBytes) continue;
|
|
678
|
+
|
|
679
|
+
const existingCall = entry.byCall.get(ck);
|
|
680
|
+
let sigs: string[];
|
|
681
|
+
if (existingCall) {
|
|
682
|
+
sigs = existingCall.signatures && existingCall.signatures.length > 0
|
|
683
|
+
? [...existingCall.signatures]
|
|
684
|
+
: [existingCall.signature];
|
|
685
|
+
if (!sigs.includes(callSig)) {
|
|
686
|
+
if (sigs.length >= MAX_SIGNATURES_PER_CALL) {
|
|
687
|
+
sigs.shift();
|
|
688
|
+
}
|
|
689
|
+
sigs.push(callSig);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
sigs = [callSig];
|
|
693
|
+
}
|
|
694
|
+
let totalSigBytes = 0;
|
|
695
|
+
for (const s of sigs) {
|
|
696
|
+
totalSigBytes += utf8.encode(s).byteLength;
|
|
697
|
+
}
|
|
698
|
+
const sizeBytes = utf8.encode(ck).byteLength + totalSigBytes;
|
|
699
|
+
if (sizeBytes > replayLimits.maxBytesPerSession) continue;
|
|
700
|
+
|
|
649
701
|
deleteReplayCall(entry, ck);
|
|
650
|
-
entry.byCall.set(ck, {
|
|
702
|
+
entry.byCall.set(ck, {
|
|
703
|
+
signature: callSig,
|
|
704
|
+
signatures: sigs,
|
|
705
|
+
sizeBytes,
|
|
706
|
+
touchedAtMs: now,
|
|
707
|
+
});
|
|
651
708
|
entry.bytes += sizeBytes;
|
|
652
709
|
replayBytes += sizeBytes;
|
|
653
710
|
inserted = true;
|
|
@@ -692,15 +749,22 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
|
|
|
692
749
|
if (!entry) {
|
|
693
750
|
return contents;
|
|
694
751
|
}
|
|
752
|
+
|
|
695
753
|
let touched = false;
|
|
696
|
-
|
|
754
|
+
// Align from the END of the recorded signature list. History may have been truncated
|
|
755
|
+
// (compaction / previous_response_id): the last remaining occurrence is the most recent call,
|
|
756
|
+
// so it must receive the newest signature. Iterate backwards and count every occurrence
|
|
757
|
+
// (signed or not) so signed Mechanism-① parts still occupy their chronological slot.
|
|
758
|
+
const reverseOccurrence = new Map<string, number>();
|
|
759
|
+
for (let ci = (contents as { role?: string; parts?: unknown[] }[]).length - 1; ci >= 0; ci--) {
|
|
760
|
+
const c = (contents as { role?: string; parts?: unknown[] }[])[ci];
|
|
697
761
|
if (!c || typeof c !== "object" || c.role !== "model" || !Array.isArray(c.parts)) continue;
|
|
698
|
-
for (
|
|
762
|
+
for (let pi = c.parts.length - 1; pi >= 0; pi--) {
|
|
763
|
+
const raw = c.parts[pi];
|
|
699
764
|
if (!raw || typeof raw !== "object") continue;
|
|
700
765
|
const part = raw as Record<string, unknown>;
|
|
701
766
|
const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined;
|
|
702
767
|
if (!fc) continue;
|
|
703
|
-
if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) continue;
|
|
704
768
|
const ck = functionCallKey(fc.name, fc.args);
|
|
705
769
|
let call = ck ? entry.byCall.get(ck) : undefined;
|
|
706
770
|
let matchedKey = ck;
|
|
@@ -717,27 +781,43 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
|
|
|
717
781
|
) {
|
|
718
782
|
try {
|
|
719
783
|
const parsedInput = JSON.parse(trimmedInput);
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
784
|
+
if (parsedInput && typeof parsedInput === "object") {
|
|
785
|
+
const altKey = functionCallKey(fc.name, parsedInput);
|
|
786
|
+
if (altKey && entry.byCall.has(altKey)) {
|
|
787
|
+
call = entry.byCall.get(altKey);
|
|
788
|
+
matchedKey = altKey;
|
|
789
|
+
}
|
|
725
790
|
}
|
|
726
|
-
}
|
|
727
791
|
} catch {
|
|
728
792
|
// not JSON, keep default
|
|
729
793
|
}
|
|
730
794
|
}
|
|
731
795
|
}
|
|
732
796
|
}
|
|
733
|
-
if (
|
|
797
|
+
if (matchedKey) {
|
|
798
|
+
const revIdx = reverseOccurrence.get(matchedKey) ?? 0;
|
|
799
|
+
reverseOccurrence.set(matchedKey, revIdx + 1);
|
|
800
|
+
if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) {
|
|
801
|
+
// Already signed (e.g. Mechanism ① or upstream response), preserve it.
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (call) {
|
|
805
|
+
const sigs = call.signatures && call.signatures.length > 0 ? call.signatures : [call.signature];
|
|
806
|
+
const chosenSig = revIdx < sigs.length
|
|
807
|
+
? sigs[sigs.length - 1 - revIdx]
|
|
808
|
+
: sigs[sigs.length - 1] ?? call.signature;
|
|
809
|
+
part.thoughtSignature = chosenSig;
|
|
810
|
+
entry.byCall.delete(matchedKey);
|
|
811
|
+
entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
|
|
812
|
+
touched = true;
|
|
813
|
+
}
|
|
814
|
+
} else if (part.thoughtSignature === undefined && part.thought_signature === undefined && call) {
|
|
734
815
|
part.thoughtSignature = call.signature;
|
|
735
|
-
entry.byCall.delete(matchedKey);
|
|
736
|
-
entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
|
|
737
816
|
touched = true;
|
|
738
817
|
}
|
|
739
818
|
}
|
|
740
819
|
}
|
|
820
|
+
|
|
741
821
|
if (touched) {
|
|
742
822
|
entry.lastActiveAtMs = now;
|
|
743
823
|
refreshReplaySessionCandidate(replayKey(model, sessionId), entry);
|
|
@@ -36,6 +36,11 @@ export const ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL = "skip_thought_signature_val
|
|
|
36
36
|
*/
|
|
37
37
|
export function isLikelyRealThoughtSignature(sig: string | undefined): boolean {
|
|
38
38
|
if (typeof sig !== "string" || sig.length < 16) return false;
|
|
39
|
+
// The validator-bypass sentinel is something WE fabricate for outbound requests when no real
|
|
40
|
+
// signature exists. It is alphanumeric with underscores, so it would otherwise satisfy every
|
|
41
|
+
// check below and be re-ingested as genuine — cached, replayed, and eventually treated as
|
|
42
|
+
// evidence that a turn was signed. It is never a real signature.
|
|
43
|
+
if (sig === ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL) return false;
|
|
39
44
|
// Reject synthetic Responses/tool-call ids and Anthropic tool-use ids (`_` or `-` separators).
|
|
40
45
|
if (/^(fc|ctc|tsc|call|msg|rs|resp|reasoning|item|ws|toolu|tool|func|function)[-_]/i.test(sig)) return false;
|
|
41
46
|
// Real Gemini thought signatures are opaque base64/base64url blobs: only [A-Za-z0-9+/_=-].
|
|
@@ -18,6 +18,43 @@ function googleErrorDetail(payloadText: string): { message?: string; status?: st
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const GOOGLE_QUOTA_EXHAUSTED_NEEDLES = [
|
|
22
|
+
"quotafailure",
|
|
23
|
+
"quota exceeded",
|
|
24
|
+
"exceeded your current quota",
|
|
25
|
+
"billing",
|
|
26
|
+
"individual quota reached",
|
|
27
|
+
"quota reached",
|
|
28
|
+
"enable overages",
|
|
29
|
+
"exhausted your capacity",
|
|
30
|
+
"daily limit reached",
|
|
31
|
+
"weekly limit reached",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Per-minute / per-second / concurrency limits are transient rate limits: they should be
|
|
35
|
+
// retried, not treated as hard quota exhaustion. These guards run before the needles so a
|
|
36
|
+
// message like "Per-minute quota exceeded" stays in the retryable bucket.
|
|
37
|
+
const GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS = [
|
|
38
|
+
"per minute",
|
|
39
|
+
"per-minute",
|
|
40
|
+
"per min",
|
|
41
|
+
"rpm",
|
|
42
|
+
"requests per minute",
|
|
43
|
+
"too many requests",
|
|
44
|
+
"rate limit",
|
|
45
|
+
"retry after",
|
|
46
|
+
// Upstream writes the header name both ways in prose ("retry-after: 60"); matching only the
|
|
47
|
+
// spaced spelling let a transient 429 fall through to the exhaustion needles below.
|
|
48
|
+
"retry-after",
|
|
49
|
+
"concurrent request limit",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export function isGoogleQuotaExhaustedText(text: string): boolean {
|
|
53
|
+
const lower = text.toLowerCase();
|
|
54
|
+
if (GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS.some(needle => lower.includes(needle))) return false;
|
|
55
|
+
return GOOGLE_QUOTA_EXHAUSTED_NEEDLES.some(needle => lower.includes(needle));
|
|
56
|
+
}
|
|
57
|
+
|
|
21
58
|
const ANTIGRAVITY_GEO_BLOCKED_MARKER = "user location is not supported for the api use";
|
|
22
59
|
|
|
23
60
|
export function isAntigravityGeoBlockedBody(payloadText: string): boolean {
|
|
@@ -27,12 +64,8 @@ export function isAntigravityGeoBlockedBody(payloadText: string): boolean {
|
|
|
27
64
|
|
|
28
65
|
function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string {
|
|
29
66
|
const lower = `${enumStatus ?? ""} ${text}`.toLowerCase();
|
|
30
|
-
const quotaExhausted =
|
|
31
|
-
|
|
32
|
-
lower.includes("quota exceeded") ||
|
|
33
|
-
lower.includes("exceeded your current quota") ||
|
|
34
|
-
lower.includes("billing");
|
|
35
|
-
if (enumStatus === "RESOURCE_EXHAUSTED" && quotaExhausted) return `${label} quota exhausted`;
|
|
67
|
+
const quotaExhausted = isGoogleQuotaExhaustedText(lower);
|
|
68
|
+
if ((!enumStatus || enumStatus === "RESOURCE_EXHAUSTED") && quotaExhausted) return `${label} quota exhausted`;
|
|
36
69
|
if (status === 429 || enumStatus === "RESOURCE_EXHAUSTED" || lower.includes("rate limit")) {
|
|
37
70
|
return `${label} rate limit exceeded`;
|
|
38
71
|
}
|
|
@@ -87,10 +120,6 @@ export function retryableGoogleStatus(status: number): boolean {
|
|
|
87
120
|
*/
|
|
88
121
|
export function isQuotaExhaustedBody(payloadText: string): boolean {
|
|
89
122
|
const { message, status } = googleErrorDetail(payloadText);
|
|
90
|
-
if (status !== "RESOURCE_EXHAUSTED") return false;
|
|
91
|
-
|
|
92
|
-
return lower.includes("quotafailure")
|
|
93
|
-
|| lower.includes("quota exceeded")
|
|
94
|
-
|| lower.includes("exceeded your current quota")
|
|
95
|
-
|| lower.includes("billing");
|
|
123
|
+
if (status && status !== "RESOURCE_EXHAUSTED") return false;
|
|
124
|
+
return isGoogleQuotaExhaustedText(message ?? payloadText);
|
|
96
125
|
}
|