@bitkyc08/opencodex 2.33.0 → 2.34.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-C4TMRloX.js +112 -0
- package/gui/dist/assets/index-DQ-Ie18T.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- 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 +162 -0
- 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-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +29 -6
- package/src/adapters/cursor/tool-definitions.ts +11 -2
- package/src/adapters/cursor/tool-result-normalize.ts +32 -1
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +189 -37
- package/src/adapters/google-antigravity-replay.ts +102 -23
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +115 -16
- 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/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -24
- 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 +32 -4
- package/src/cli/account-extended.ts +1 -1
- 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/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/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 +46 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- 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 -3
- 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 +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +104 -25
- 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/rebase-provenance.ts +68 -0
- package/src/config.ts +107 -9
- package/src/generated/compatibility-version.json +191 -139
- 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/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/types.ts +15 -0
- 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 +65 -7
- 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 +4 -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/index.ts +77 -11
- 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/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +6 -0
- 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 +41 -15
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +494 -114
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +2 -1
- 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-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 +14 -2
- package/src/types/config.ts +53 -0
- package/src/types/provider.ts +64 -0
- package/src/types/request.ts +11 -3
- package/src/types/tools.ts +7 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +3 -0
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-23-Lf7jR.js +0 -102
- package/gui/dist/assets/index-DxJMDyOr.css +0 -1
package/src/adapters/cursor.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
|
|
|
9
9
|
import { mapCursorServerMessage } from "./cursor/message-mapper";
|
|
10
10
|
import {
|
|
11
11
|
createCursorRequest,
|
|
12
|
+
cursorClientThreadOwner,
|
|
12
13
|
cursorCoveredPrefixDigest,
|
|
13
14
|
cursorInstructionDigest,
|
|
14
15
|
} from "./cursor/request-builder";
|
|
@@ -28,6 +29,15 @@ import { debugProviderDiagnostic } from "../lib/debug";
|
|
|
28
29
|
import { estimateTokens } from "../lib/token-estimate";
|
|
29
30
|
import { rememberCursorThreadConversation } from "./cursor/thread-continuity";
|
|
30
31
|
import { runCursorTurnWithRetry } from "./cursor/transport-retry";
|
|
32
|
+
import { cursorRequestHasShellAlias, cursorRequestUsesCodeMode } from "./cursor/tool-definitions";
|
|
33
|
+
import {
|
|
34
|
+
CURSOR_ECHO_RETRY_CONTINUATION_TEXT,
|
|
35
|
+
CURSOR_ROUTING_COMMENTARY_RETRY_TEXT,
|
|
36
|
+
CursorEnvelopeEchoSniffer,
|
|
37
|
+
CursorRoutingCommentaryError,
|
|
38
|
+
CursorRoutingCommentarySniffer,
|
|
39
|
+
CursorToolResultEchoError,
|
|
40
|
+
} from "./cursor/envelope-echo";
|
|
31
41
|
import {
|
|
32
42
|
createDisabledCursorTransport,
|
|
33
43
|
CursorTransportDisabledError,
|
|
@@ -145,15 +155,36 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
145
155
|
let completedNormally = false;
|
|
146
156
|
let lastTransport: { captured?: Uint8Array } | undefined;
|
|
147
157
|
let emittedClientTool = false;
|
|
158
|
+
// Ordering proof for tool-suspended checkpoints: true only when the newest captured
|
|
159
|
+
// checkpoint bytes arrived AFTER the turn emitted a client tool call, i.e. upstream
|
|
160
|
+
// serialized its suspended-on-tool-call state. Only that snapshot can safely resume
|
|
161
|
+
// with the covered-prefix + trailing-toolResult path (devlog 260826 050).
|
|
162
|
+
let capturedAfterClientTool = false;
|
|
148
163
|
|
|
149
164
|
const commitCapturedCheckpoint = (activeRequest: ReturnType<typeof createCursorRequest>): void => {
|
|
165
|
+
const toolSuspendedCommit =
|
|
166
|
+
emittedClientTool
|
|
167
|
+
&& capturedAfterClientTool
|
|
168
|
+
&& isCursorExternalWireModel(activeRequest.modelId);
|
|
150
169
|
if (
|
|
151
170
|
replayUnsafe
|
|
152
|
-
|| emittedClientTool
|
|
171
|
+
|| (emittedClientTool && !toolSuspendedCommit)
|
|
153
172
|
|| activeRequest.contextUsageStoreCheckpoints === false
|
|
154
173
|
|| !lastTransport?.captured
|
|
155
174
|
|| lastTransport.captured.byteLength === 0
|
|
156
|
-
)
|
|
175
|
+
) {
|
|
176
|
+
// Refusal diagnostics (devlog 260826 050/080): name the exact guard so a live
|
|
177
|
+
// missing_ref chain can be attributed without instrumented rebuilds.
|
|
178
|
+
debugProviderDiagnostic("cursor", "checkpoint-commit-refused", {
|
|
179
|
+
replayUnsafe,
|
|
180
|
+
emittedClientTool,
|
|
181
|
+
capturedAfterClientTool,
|
|
182
|
+
externalModel: isCursorExternalWireModel(activeRequest.modelId),
|
|
183
|
+
storeCheckpoints: activeRequest.contextUsageStoreCheckpoints !== false,
|
|
184
|
+
capturedBytes: lastTransport?.captured?.byteLength ?? 0,
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
157
188
|
const previousRef = _parsed._providerContinuation?.cursor?.checkpointRef;
|
|
158
189
|
const coveredMessageCount = _parsed.context.messages.length;
|
|
159
190
|
const checkpointRef = commitCursorCheckpoint({
|
|
@@ -172,7 +203,9 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
172
203
|
cursor: {
|
|
173
204
|
...(_parsed._providerContinuation?.cursor ?? {}),
|
|
174
205
|
conversationId: activeRequest.conversationId,
|
|
175
|
-
|
|
206
|
+
// A tool-suspended checkpoint is only usable by the immediate trailing-toolResult
|
|
207
|
+
// continuation; the request-builder guard keys on checkpointUsable=false for that.
|
|
208
|
+
checkpointUsable: !toolSuspendedCommit,
|
|
176
209
|
checkpointRef,
|
|
177
210
|
},
|
|
178
211
|
};
|
|
@@ -182,10 +215,43 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
182
215
|
checkpointRefHash: cursorCheckpointRefHash(checkpointRef),
|
|
183
216
|
checkpointBytes: lastTransport.captured.byteLength,
|
|
184
217
|
wireModel: activeRequest.modelId,
|
|
218
|
+
...(toolSuspendedCommit ? { toolSuspended: true } : {}),
|
|
185
219
|
});
|
|
186
220
|
};
|
|
187
221
|
|
|
188
222
|
const runOnce = async (activeRequest: ReturnType<typeof createCursorRequest>) => {
|
|
223
|
+
// Envelope echo quarantine (devlog 260826 gap-10): external full-replay continuations
|
|
224
|
+
// whose trailing input is a tool result sometimes ECHO the replayed "[Tool Result]"
|
|
225
|
+
// envelope as assistant text (kimi-k3 ~30-40% of multi-round probes). Hold the first
|
|
226
|
+
// text deltas until they provably diverge from the markers; a completed marker turns
|
|
227
|
+
// the turn into a retryable semantic failure BEFORE any client-visible delta escapes.
|
|
228
|
+
// Armed for ANY external turn whose replayed history contains a tool result — echo
|
|
229
|
+
// priming was observed live on user-action rounds too (the envelope lives in the
|
|
230
|
+
// flattened history regardless of which role ends the input).
|
|
231
|
+
const armEchoSniffer =
|
|
232
|
+
isCursorExternalWireModel(activeRequest.modelId)
|
|
233
|
+
&& (_parsed.context.messages ?? []).some(message => message.role === "toolResult");
|
|
234
|
+
const echoSniffer = armEchoSniffer ? new CursorEnvelopeEchoSniffer() : undefined;
|
|
235
|
+
const armRoutingCommentarySniffer =
|
|
236
|
+
isCursorExternalWireModel(activeRequest.modelId)
|
|
237
|
+
&& (
|
|
238
|
+
cursorRequestUsesCodeMode(activeRequest.tools, activeRequest.toolChoice)
|
|
239
|
+
|| cursorRequestHasShellAlias(activeRequest.tools)
|
|
240
|
+
);
|
|
241
|
+
const routingCommentarySniffer = armRoutingCommentarySniffer
|
|
242
|
+
? new CursorRoutingCommentarySniffer()
|
|
243
|
+
: undefined;
|
|
244
|
+
let guardHeld: AdapterEvent[] = [];
|
|
245
|
+
const releaseGuardHeld = () => {
|
|
246
|
+
for (const held of guardHeld) {
|
|
247
|
+
if (held.type !== "heartbeat") emittedOutput = true;
|
|
248
|
+
emit(held);
|
|
249
|
+
}
|
|
250
|
+
guardHeld = [];
|
|
251
|
+
};
|
|
252
|
+
const guardsSettled = () =>
|
|
253
|
+
(!echoSniffer || echoSniffer.settled)
|
|
254
|
+
&& (!routingCommentarySniffer || routingCommentarySniffer.settled);
|
|
189
255
|
await runCursorTurnWithRetry(
|
|
190
256
|
makeTransport,
|
|
191
257
|
{
|
|
@@ -207,14 +273,54 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
207
273
|
if (message.type === "done") completedNormally = true;
|
|
208
274
|
if (message.type === "tool_call_end") emittedClientTool = true;
|
|
209
275
|
const captured = capturedCursorCheckpointBytes(activeTransport);
|
|
210
|
-
if (captured)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
276
|
+
if (captured) {
|
|
277
|
+
if (captured !== lastTransport?.captured) capturedAfterClientTool = emittedClientTool;
|
|
278
|
+
lastTransport = { captured };
|
|
279
|
+
}
|
|
280
|
+
const events = mapCursorServerMessage(message, {
|
|
281
|
+
kv,
|
|
282
|
+
writeClient: clientMessage => {
|
|
283
|
+
void activeTransport.writeClient(clientMessage);
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
for (const event of events) {
|
|
287
|
+
if (!guardsSettled()) {
|
|
288
|
+
if (event.type === "text_delta") {
|
|
289
|
+
guardHeld.push(event);
|
|
290
|
+
if (echoSniffer && !echoSniffer.settled) {
|
|
291
|
+
const decision = echoSniffer.feed(event.text);
|
|
292
|
+
if (decision.kind === "echo") {
|
|
293
|
+
guardHeld = [];
|
|
294
|
+
throw new CursorToolResultEchoError(decision.marker);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (routingCommentarySniffer && !routingCommentarySniffer.settled) {
|
|
298
|
+
const decision = routingCommentarySniffer.feed(event.text);
|
|
299
|
+
if (decision.kind === "hallucination") {
|
|
300
|
+
guardHeld = [];
|
|
301
|
+
throw new CursorRoutingCommentaryError();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (guardsSettled()) releaseGuardHeld();
|
|
305
|
+
continue;
|
|
306
|
+
} else if (event.type === "thinking_delta" || event.type === "heartbeat") {
|
|
307
|
+
// Reasoning before first text stays ordered; liveness still passes through.
|
|
308
|
+
if (event.type === "thinking_delta") {
|
|
309
|
+
guardHeld.push(event);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
// A tool call, done, or error closes the text quarantine. Re-check the full
|
|
314
|
+
// held first line before release so a fragmented routing claim cannot leak.
|
|
315
|
+
echoSniffer?.finish();
|
|
316
|
+
const routingDecision = routingCommentarySniffer?.finish();
|
|
317
|
+
if (routingDecision?.kind === "hallucination") {
|
|
318
|
+
guardHeld = [];
|
|
319
|
+
throw new CursorRoutingCommentaryError();
|
|
320
|
+
}
|
|
321
|
+
releaseGuardHeld();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
218
324
|
if (event.type !== "heartbeat") emittedOutput = true;
|
|
219
325
|
if (event.type === "done") {
|
|
220
326
|
commitCapturedCheckpoint(activeRequest);
|
|
@@ -246,36 +352,82 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
|
|
|
246
352
|
try {
|
|
247
353
|
await runOnce(request);
|
|
248
354
|
} catch (err) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
355
|
+
const outputGuardRetryText =
|
|
356
|
+
err instanceof CursorToolResultEchoError
|
|
357
|
+
? CURSOR_ECHO_RETRY_CONTINUATION_TEXT
|
|
358
|
+
: err instanceof CursorRoutingCommentaryError
|
|
359
|
+
? CURSOR_ROUTING_COMMENTARY_RETRY_TEXT
|
|
360
|
+
: undefined;
|
|
361
|
+
// One-shot corrective retry for guarded external output (devlog 260826 gap-10/11).
|
|
362
|
+
// The quarantine guarantees no client-visible delta escaped, so a fresh-conversation
|
|
363
|
+
// retry is safe. A second rejection propagates as an error rather than looping.
|
|
252
364
|
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|| replayUnsafe
|
|
258
|
-
|| incoming.abortSignal?.aborted
|
|
365
|
+
outputGuardRetryText
|
|
366
|
+
&& !emittedOutput
|
|
367
|
+
&& !replayUnsafe
|
|
368
|
+
&& !incoming.abortSignal?.aborted
|
|
259
369
|
) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// next turn does not recompute the stale deterministic thread hash. Isolated helper /
|
|
270
|
-
// compaction turns must not park their throwaway id under the parent thread key.
|
|
271
|
-
if (_parsed._clientThreadId && _parsed._cursorIsolateConversation !== true) {
|
|
272
|
-
rememberCursorThreadConversation(
|
|
273
|
-
_parsed._clientThreadId,
|
|
274
|
-
request.conversationId,
|
|
275
|
-
_parsed._cursorIdentityScope,
|
|
370
|
+
debugProviderDiagnostic(
|
|
371
|
+
"cursor",
|
|
372
|
+
err instanceof CursorToolResultEchoError
|
|
373
|
+
? "envelope-echo-retry"
|
|
374
|
+
: "routing-commentary-retry",
|
|
375
|
+
{
|
|
376
|
+
wireModel: request.modelId,
|
|
377
|
+
conversationHash: request.conversationId.slice(0, 16),
|
|
378
|
+
},
|
|
276
379
|
);
|
|
380
|
+
const echoedConversationId = request.conversationId;
|
|
381
|
+
lastTransport = undefined;
|
|
382
|
+
_parsed._cursorConversationId = undefined;
|
|
383
|
+
request = {
|
|
384
|
+
...createCursorRequest(_parsed, { forceFreshConversation: true }),
|
|
385
|
+
echoRetryContinuationText: outputGuardRetryText,
|
|
386
|
+
};
|
|
387
|
+
rekeyContextUsage(echoedConversationId, request.conversationId);
|
|
388
|
+
_parsed._cursorConversationId = request.conversationId;
|
|
389
|
+
const echoThreadOwner = cursorClientThreadOwner(_parsed);
|
|
390
|
+
if (echoThreadOwner && _parsed._cursorIsolateConversation !== true) {
|
|
391
|
+
rememberCursorThreadConversation(
|
|
392
|
+
echoThreadOwner,
|
|
393
|
+
request.conversationId,
|
|
394
|
+
_parsed._cursorIdentityScope,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
await runOnce(request);
|
|
398
|
+
} else {
|
|
399
|
+
// One-shot fallback for external-model Connect invalid_argument before any
|
|
400
|
+
// non-heartbeat output. Retries apply only to safe plain-user turns; tool-result
|
|
401
|
+
// resumes, local exec/MCP side effects, and already-emitted output fail closed.
|
|
402
|
+
if (
|
|
403
|
+
!isCursorInvalidArgumentError(err)
|
|
404
|
+
|| !isCursorExternalWireModel(request.modelId)
|
|
405
|
+
|| lastRawIsToolResult
|
|
406
|
+
|| emittedOutput
|
|
407
|
+
|| replayUnsafe
|
|
408
|
+
|| incoming.abortSignal?.aborted
|
|
409
|
+
) {
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
const failedConversationId = request.conversationId;
|
|
413
|
+
lastTransport = undefined;
|
|
414
|
+
_parsed._cursorConversationId = undefined;
|
|
415
|
+
request = createCursorRequest(_parsed, { forceFreshConversation: true });
|
|
416
|
+
rekeyContextUsage(failedConversationId, request.conversationId);
|
|
417
|
+
_parsed._cursorConversationId = request.conversationId;
|
|
418
|
+
// Persist recovery for store:false clients that send any stable Cursor thread owner, so
|
|
419
|
+
// the next turn does not recompute the stale deterministic thread hash. Isolated helper /
|
|
420
|
+
// compaction turns must not park their throwaway id under the parent or Desktop owner.
|
|
421
|
+
const threadOwner = cursorClientThreadOwner(_parsed);
|
|
422
|
+
if (threadOwner && _parsed._cursorIsolateConversation !== true) {
|
|
423
|
+
rememberCursorThreadConversation(
|
|
424
|
+
threadOwner,
|
|
425
|
+
request.conversationId,
|
|
426
|
+
_parsed._cursorIdentityScope,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
await runOnce(request);
|
|
277
430
|
}
|
|
278
|
-
await runOnce(request);
|
|
279
431
|
}
|
|
280
432
|
if (
|
|
281
433
|
request.checkpointInvalidationReason
|
|
@@ -19,6 +19,7 @@ import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
|
19
19
|
|
|
20
20
|
interface ReplayCall {
|
|
21
21
|
signature: string;
|
|
22
|
+
signatures?: string[];
|
|
22
23
|
sizeBytes: number;
|
|
23
24
|
touchedAtMs: number;
|
|
24
25
|
}
|
|
@@ -34,6 +35,7 @@ interface ReplayEntry {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const MIN_SIGNATURE_LEN = 16;
|
|
38
|
+
const MAX_SIGNATURES_PER_CALL = 32;
|
|
37
39
|
const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h
|
|
38
40
|
export const ANTIGRAVITY_REPLAY_MAX_ENTRIES = 10_240;
|
|
39
41
|
const REPLAY_EVICT_BATCH = 128;
|
|
@@ -103,16 +105,38 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
|
|
|
103
105
|
for (const pair of rec.byCall) {
|
|
104
106
|
if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string") continue;
|
|
105
107
|
if (!/^[0-9a-f]{64}$/.test(pair[0])) continue;
|
|
106
|
-
const call = pair[1] as { signature?: unknown; touchedAtMs?: unknown } | null;
|
|
108
|
+
const call = pair[1] as { signature?: unknown; signatures?: unknown; touchedAtMs?: unknown } | null;
|
|
107
109
|
if (!call || typeof call !== "object" || Array.isArray(call)) continue;
|
|
108
110
|
if (typeof call.signature !== "string" || call.signature.length < MIN_SIGNATURE_LEN) continue;
|
|
109
111
|
if (typeof call.touchedAtMs !== "number" || !Number.isFinite(call.touchedAtMs)) continue;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
let sigs: string[] = [];
|
|
113
|
+
if (Array.isArray(call.signatures)) {
|
|
114
|
+
for (const s of call.signatures) {
|
|
115
|
+
if (typeof s === "string" && s.length >= MIN_SIGNATURE_LEN) {
|
|
116
|
+
sigs.push(s);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (sigs.length === 0) {
|
|
121
|
+
sigs = [call.signature];
|
|
122
|
+
} else if (!sigs.includes(call.signature)) {
|
|
123
|
+
sigs.push(call.signature);
|
|
124
|
+
}
|
|
125
|
+
if (sigs.length > MAX_SIGNATURES_PER_CALL) {
|
|
126
|
+
sigs = sigs.slice(-MAX_SIGNATURES_PER_CALL);
|
|
127
|
+
}
|
|
128
|
+
let totalSigBytes = 0;
|
|
129
|
+
let anySigOversized = false;
|
|
130
|
+
for (const s of sigs) {
|
|
131
|
+
const sb = utf8.encode(s).byteLength;
|
|
132
|
+
if (sb > replayLimits.maxSignatureBytes) {
|
|
133
|
+
anySigOversized = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
totalSigBytes += sb;
|
|
137
|
+
}
|
|
138
|
+
if (anySigOversized) continue;
|
|
139
|
+
const callBytes = utf8.encode(pair[0]).byteLength + totalSigBytes;
|
|
116
140
|
if (callBytes > replayLimits.maxBytesPerSession) continue;
|
|
117
141
|
// A duplicated call key would overstate entry.bytes (the map keeps only the
|
|
118
142
|
// last value) and could evict valid sessions; keep the first occurrence.
|
|
@@ -122,6 +146,7 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
|
|
|
122
146
|
}
|
|
123
147
|
byCall.set(pair[0], {
|
|
124
148
|
signature: call.signature,
|
|
149
|
+
signatures: sigs,
|
|
125
150
|
sizeBytes: callBytes,
|
|
126
151
|
touchedAtMs: call.touchedAtMs,
|
|
127
152
|
});
|
|
@@ -226,7 +251,11 @@ async function persistReplaySnapshotNow(): Promise<void> {
|
|
|
226
251
|
for (const [key, entry] of [...replayCache].sort((a, b) => b[1].lastActiveAtMs - a[1].lastActiveAtMs)) {
|
|
227
252
|
const byCall = [...entry.byCall].map(([callKey, call]) => [
|
|
228
253
|
callKey,
|
|
229
|
-
{
|
|
254
|
+
{
|
|
255
|
+
signature: call.signature,
|
|
256
|
+
...(call.signatures && call.signatures.length > 1 ? { signatures: call.signatures } : {}),
|
|
257
|
+
touchedAtMs: call.touchedAtMs,
|
|
258
|
+
},
|
|
230
259
|
]);
|
|
231
260
|
const persistEntry: [string, unknown] = [key, {
|
|
232
261
|
byCall,
|
|
@@ -644,10 +673,37 @@ export function observeAntigravityReplay(
|
|
|
644
673
|
const ck = functionCallKey(fc.name, fc.args);
|
|
645
674
|
if (!ck) continue; // only function-call signatures are replayable by identity
|
|
646
675
|
const signatureBytes = utf8.encode(callSig).byteLength;
|
|
647
|
-
|
|
648
|
-
|
|
676
|
+
if (signatureBytes > replayLimits.maxSignatureBytes) continue;
|
|
677
|
+
|
|
678
|
+
const existingCall = entry.byCall.get(ck);
|
|
679
|
+
let sigs: string[];
|
|
680
|
+
if (existingCall) {
|
|
681
|
+
sigs = existingCall.signatures && existingCall.signatures.length > 0
|
|
682
|
+
? [...existingCall.signatures]
|
|
683
|
+
: [existingCall.signature];
|
|
684
|
+
if (!sigs.includes(callSig)) {
|
|
685
|
+
if (sigs.length >= MAX_SIGNATURES_PER_CALL) {
|
|
686
|
+
sigs.shift();
|
|
687
|
+
}
|
|
688
|
+
sigs.push(callSig);
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
sigs = [callSig];
|
|
692
|
+
}
|
|
693
|
+
let totalSigBytes = 0;
|
|
694
|
+
for (const s of sigs) {
|
|
695
|
+
totalSigBytes += utf8.encode(s).byteLength;
|
|
696
|
+
}
|
|
697
|
+
const sizeBytes = utf8.encode(ck).byteLength + totalSigBytes;
|
|
698
|
+
if (sizeBytes > replayLimits.maxBytesPerSession) continue;
|
|
699
|
+
|
|
649
700
|
deleteReplayCall(entry, ck);
|
|
650
|
-
entry.byCall.set(ck, {
|
|
701
|
+
entry.byCall.set(ck, {
|
|
702
|
+
signature: callSig,
|
|
703
|
+
signatures: sigs,
|
|
704
|
+
sizeBytes,
|
|
705
|
+
touchedAtMs: now,
|
|
706
|
+
});
|
|
651
707
|
entry.bytes += sizeBytes;
|
|
652
708
|
replayBytes += sizeBytes;
|
|
653
709
|
inserted = true;
|
|
@@ -692,15 +748,22 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
|
|
|
692
748
|
if (!entry) {
|
|
693
749
|
return contents;
|
|
694
750
|
}
|
|
751
|
+
|
|
695
752
|
let touched = false;
|
|
696
|
-
|
|
753
|
+
// Align from the END of the recorded signature list. History may have been truncated
|
|
754
|
+
// (compaction / previous_response_id): the last remaining occurrence is the most recent call,
|
|
755
|
+
// so it must receive the newest signature. Iterate backwards and count every occurrence
|
|
756
|
+
// (signed or not) so signed Mechanism-① parts still occupy their chronological slot.
|
|
757
|
+
const reverseOccurrence = new Map<string, number>();
|
|
758
|
+
for (let ci = (contents as { role?: string; parts?: unknown[] }[]).length - 1; ci >= 0; ci--) {
|
|
759
|
+
const c = (contents as { role?: string; parts?: unknown[] }[])[ci];
|
|
697
760
|
if (!c || typeof c !== "object" || c.role !== "model" || !Array.isArray(c.parts)) continue;
|
|
698
|
-
for (
|
|
761
|
+
for (let pi = c.parts.length - 1; pi >= 0; pi--) {
|
|
762
|
+
const raw = c.parts[pi];
|
|
699
763
|
if (!raw || typeof raw !== "object") continue;
|
|
700
764
|
const part = raw as Record<string, unknown>;
|
|
701
765
|
const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined;
|
|
702
766
|
if (!fc) continue;
|
|
703
|
-
if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) continue;
|
|
704
767
|
const ck = functionCallKey(fc.name, fc.args);
|
|
705
768
|
let call = ck ? entry.byCall.get(ck) : undefined;
|
|
706
769
|
let matchedKey = ck;
|
|
@@ -717,27 +780,43 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
|
|
|
717
780
|
) {
|
|
718
781
|
try {
|
|
719
782
|
const parsedInput = JSON.parse(trimmedInput);
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
783
|
+
if (parsedInput && typeof parsedInput === "object") {
|
|
784
|
+
const altKey = functionCallKey(fc.name, parsedInput);
|
|
785
|
+
if (altKey && entry.byCall.has(altKey)) {
|
|
786
|
+
call = entry.byCall.get(altKey);
|
|
787
|
+
matchedKey = altKey;
|
|
788
|
+
}
|
|
725
789
|
}
|
|
726
|
-
}
|
|
727
790
|
} catch {
|
|
728
791
|
// not JSON, keep default
|
|
729
792
|
}
|
|
730
793
|
}
|
|
731
794
|
}
|
|
732
795
|
}
|
|
733
|
-
if (
|
|
796
|
+
if (matchedKey) {
|
|
797
|
+
const revIdx = reverseOccurrence.get(matchedKey) ?? 0;
|
|
798
|
+
reverseOccurrence.set(matchedKey, revIdx + 1);
|
|
799
|
+
if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) {
|
|
800
|
+
// Already signed (e.g. Mechanism ① or upstream response), preserve it.
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
if (call) {
|
|
804
|
+
const sigs = call.signatures && call.signatures.length > 0 ? call.signatures : [call.signature];
|
|
805
|
+
const chosenSig = revIdx < sigs.length
|
|
806
|
+
? sigs[sigs.length - 1 - revIdx]
|
|
807
|
+
: sigs[sigs.length - 1] ?? call.signature;
|
|
808
|
+
part.thoughtSignature = chosenSig;
|
|
809
|
+
entry.byCall.delete(matchedKey);
|
|
810
|
+
entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
|
|
811
|
+
touched = true;
|
|
812
|
+
}
|
|
813
|
+
} else if (part.thoughtSignature === undefined && part.thought_signature === undefined && call) {
|
|
734
814
|
part.thoughtSignature = call.signature;
|
|
735
|
-
entry.byCall.delete(matchedKey);
|
|
736
|
-
entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
|
|
737
815
|
touched = true;
|
|
738
816
|
}
|
|
739
817
|
}
|
|
740
818
|
}
|
|
819
|
+
|
|
741
820
|
if (touched) {
|
|
742
821
|
entry.lastActiveAtMs = now;
|
|
743
822
|
refreshReplaySessionCandidate(replayKey(model, sessionId), entry);
|
|
@@ -15,14 +15,50 @@ function googleErrorDetail(payloadText: string): { message?: string; status?: st
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const GOOGLE_QUOTA_EXHAUSTED_NEEDLES = [
|
|
21
|
+
"quotafailure",
|
|
22
|
+
"quota exceeded",
|
|
23
|
+
"exceeded your current quota",
|
|
24
|
+
"billing",
|
|
25
|
+
"individual quota reached",
|
|
26
|
+
"quota reached",
|
|
27
|
+
"enable overages",
|
|
28
|
+
"exhausted your capacity",
|
|
29
|
+
"daily limit reached",
|
|
30
|
+
"weekly limit reached",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Per-minute / per-second / concurrency limits are transient rate limits: they should be
|
|
34
|
+
// retried, not treated as hard quota exhaustion. These guards run before the needles so a
|
|
35
|
+
// message like "Per-minute quota exceeded" stays in the retryable bucket.
|
|
36
|
+
const GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS = [
|
|
37
|
+
"per minute",
|
|
38
|
+
"per-minute",
|
|
39
|
+
"per min",
|
|
40
|
+
"rpm",
|
|
41
|
+
"requests per minute",
|
|
42
|
+
"too many requests",
|
|
43
|
+
"rate limit",
|
|
44
|
+
"retry after",
|
|
45
|
+
// Upstream writes the header name both ways in prose ("retry-after: 60"); matching only the
|
|
46
|
+
// spaced spelling let a transient 429 fall through to the exhaustion needles below.
|
|
47
|
+
"retry-after",
|
|
48
|
+
"concurrent request limit",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export function isGoogleQuotaExhaustedText(text: string): boolean {
|
|
52
|
+
const lower = text.toLowerCase();
|
|
53
|
+
if (GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS.some(needle => lower.includes(needle))) return false;
|
|
54
|
+
return GOOGLE_QUOTA_EXHAUSTED_NEEDLES.some(needle => lower.includes(needle));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
18
58
|
function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string {
|
|
19
59
|
const lower = `${enumStatus ?? ""} ${text}`.toLowerCase();
|
|
20
|
-
const quotaExhausted =
|
|
21
|
-
|
|
22
|
-
lower.includes("quota exceeded") ||
|
|
23
|
-
lower.includes("exceeded your current quota") ||
|
|
24
|
-
lower.includes("billing");
|
|
25
|
-
if (enumStatus === "RESOURCE_EXHAUSTED" && quotaExhausted) return `${label} quota exhausted`;
|
|
60
|
+
const quotaExhausted = isGoogleQuotaExhaustedText(lower);
|
|
61
|
+
if ((!enumStatus || enumStatus === "RESOURCE_EXHAUSTED") && quotaExhausted) return `${label} quota exhausted`;
|
|
26
62
|
if (status === 429 || enumStatus === "RESOURCE_EXHAUSTED" || lower.includes("rate limit")) {
|
|
27
63
|
return `${label} rate limit exceeded`;
|
|
28
64
|
}
|
|
@@ -76,10 +112,6 @@ export function retryableGoogleStatus(status: number): boolean {
|
|
|
76
112
|
*/
|
|
77
113
|
export function isQuotaExhaustedBody(payloadText: string): boolean {
|
|
78
114
|
const { message, status } = googleErrorDetail(payloadText);
|
|
79
|
-
if (status !== "RESOURCE_EXHAUSTED") return false;
|
|
80
|
-
|
|
81
|
-
return lower.includes("quotafailure")
|
|
82
|
-
|| lower.includes("quota exceeded")
|
|
83
|
-
|| lower.includes("exceeded your current quota")
|
|
84
|
-
|| lower.includes("billing");
|
|
115
|
+
if (status && status !== "RESOURCE_EXHAUSTED") return false;
|
|
116
|
+
return isGoogleQuotaExhaustedText(message ?? payloadText);
|
|
85
117
|
}
|