@iislee/opencodex 2.31.0 → 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/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -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 +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- 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/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- 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 +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- 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/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -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/label.ts +34 -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 +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -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 +27 -7
- 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 +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- 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 +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- 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 +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CANONICAL_EFFORT_SUFFIXES,
|
|
3
3
|
cursorModelEffortLadder,
|
|
4
|
+
cursorModelHasEffortTiers,
|
|
4
5
|
cursorWireModelIdWithEffort,
|
|
6
|
+
CURSOR_THINKING_MODEL_IDS,
|
|
5
7
|
} from "./effort-map";
|
|
8
|
+
import { parseCursorVariantId } from "./catalog";
|
|
6
9
|
|
|
7
10
|
export interface CursorModelInfo {
|
|
8
11
|
id: string;
|
|
@@ -74,9 +77,16 @@ function stripCursorWirePrefix(id: string): string {
|
|
|
74
77
|
* ordinary `{base}-{effort}` form, or Cursor's current `{base-without-fast}-{effort}-fast` form.
|
|
75
78
|
*/
|
|
76
79
|
export function isCursorModelAvailableForAccount(modelId: string, liveIds: readonly string[]): boolean {
|
|
80
|
+
// Umbrella matching (devlog 260828_cursor_umbrella_catalog): a live suffix
|
|
81
|
+
// id counts toward its BASE — any variant dimension (thinking/fast/effort)
|
|
82
|
+
// proves the account can reach the umbrella. Unknown ids fall back to the
|
|
83
|
+
// legacy exact/suffix comparison so non-cataloged rows keep matching.
|
|
84
|
+
const parsedTarget = parseCursorVariantId(modelId);
|
|
77
85
|
return liveIds.some(raw => {
|
|
78
86
|
const id = stripCursorWirePrefix(raw);
|
|
79
87
|
if (id === modelId) return true;
|
|
88
|
+
const parsedLive = parseCursorVariantId(id);
|
|
89
|
+
if (parsedLive.known && parsedTarget.known && parsedLive.baseId === parsedTarget.baseId) return true;
|
|
80
90
|
for (const effort of CANONICAL_EFFORT_SUFFIXES) {
|
|
81
91
|
if (
|
|
82
92
|
id === `${modelId}-${effort}` ||
|
|
@@ -152,6 +162,25 @@ export function cursorCodexToWireModelId(modelId: string): string {
|
|
|
152
162
|
return cursorWireModelSelection(modelId).modelId;
|
|
153
163
|
}
|
|
154
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Synthetic ultra/big-context picker marker (devlog 260826 070). A `cursor/<base>-1m` row is a
|
|
167
|
+
* picker-only variant: the wire request keeps `<base>` (plus effort suffix) and turns on Cursor
|
|
168
|
+
* Max Mode instead. Only ids listed here are treated as synthetic — a real upstream wire id that
|
|
169
|
+
* happens to end in `-1m` never collides because it will not be in this set.
|
|
170
|
+
*/
|
|
171
|
+
export const CURSOR_ULTRA_1M_MODEL_IDS: ReadonlySet<string> = new Set([
|
|
172
|
+
"kimi-k3-1m",
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const CURSOR_ULTRA_1M_SUFFIX = "-1m";
|
|
176
|
+
|
|
177
|
+
/** Resolve a synthetic ultra marker id to its wire base, or undefined for ordinary ids. */
|
|
178
|
+
export function cursorUltraBaseModelId(modelId: string): string | undefined {
|
|
179
|
+
const normalized = modelId.startsWith("cursor/") ? modelId.slice("cursor/".length) : modelId;
|
|
180
|
+
if (!CURSOR_ULTRA_1M_MODEL_IDS.has(normalized)) return undefined;
|
|
181
|
+
return normalized.slice(0, -CURSOR_ULTRA_1M_SUFFIX.length);
|
|
182
|
+
}
|
|
183
|
+
|
|
155
184
|
/**
|
|
156
185
|
* Cursor-native wire models keep server-side conversation state reliably.
|
|
157
186
|
* External models (gpt/claude/gemini/grok families and similar) are more brittle on resumeAction.
|
|
@@ -212,10 +241,25 @@ export function filterCursorConfiguredModelsByLiveDiscovery<T extends { id: stri
|
|
|
212
241
|
liveIds: readonly string[],
|
|
213
242
|
): T[] {
|
|
214
243
|
return configured.filter(model =>
|
|
215
|
-
|
|
244
|
+
!CURSOR_KNOWN_UNCALLABLE_MODEL_IDS.has(model.id)
|
|
245
|
+
&& (
|
|
246
|
+
isCursorRouterModelId(model.id)
|
|
247
|
+
// Synthetic ultra rows ride their base model's account availability.
|
|
248
|
+
|| isCursorModelAvailableForAccount(cursorUltraBaseModelId(model.id) ?? model.id, liveIds)
|
|
249
|
+
),
|
|
216
250
|
);
|
|
217
251
|
}
|
|
218
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Models GetUsableModels advertises but whose every Run returns not_found (catalog honesty,
|
|
255
|
+
* devlog 260826_cursor_responses_gap 060). The claude-opus-5 REGULAR wire family is the known
|
|
256
|
+
* case (probes 2026-08-26: 100% not_found while -fast/-thinking succeed) — under the umbrella
|
|
257
|
+
* catalog (devlog 260828) that quarantine moved to the RESOLVER level: the capability marks the
|
|
258
|
+
* regular VARIANT quarantined, the bare slug routes the healthy thinking variant, and the base
|
|
259
|
+
* row stays in the seed. This row-level set stays for future whole-base quarantines.
|
|
260
|
+
*/
|
|
261
|
+
export const CURSOR_KNOWN_UNCALLABLE_MODEL_IDS: ReadonlySet<string> = new Set([]);
|
|
262
|
+
|
|
219
263
|
export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorModels([
|
|
220
264
|
// Context windows and the model lineup mirror Cursor's public models/pricing docs plus the jawcode
|
|
221
265
|
// SOT (../jawcode/packages/ai/src/models.json, `cursor` provider), which mirrors the real
|
|
@@ -227,25 +271,26 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
227
271
|
// gemini/grok/kimi-k2.7/gpt-5-mini are reasoning models in the SOT but are sent bare (no tier picker).
|
|
228
272
|
...CURSOR_ROUTER_MODEL_IDS.map(id => ({ id, contextWindow: CONTEXT_200K, supportsReasoningEffort: false })),
|
|
229
273
|
|
|
230
|
-
|
|
274
|
+
// Umbrella seed (devlog 260828_cursor_umbrella_catalog): one row per BASE
|
|
275
|
+
// model. Thinking merges into the base (the resolver routes the thinking
|
|
276
|
+
// variant); fast / thinking-fast / -1m stay routable as aliases but add no
|
|
277
|
+
// rows. Windows follow CURSOR_CAPABILITIES where the base is cataloged.
|
|
278
|
+
{ id: "claude-sonnet-5", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
231
279
|
{ id: "claude-4-sonnet", contextWindow: CONTEXT_200K },
|
|
232
280
|
{ id: "claude-4-sonnet-1m", contextWindow: CONTEXT_1M },
|
|
233
281
|
{ id: "claude-4.5-haiku", contextWindow: CONTEXT_200K },
|
|
234
282
|
{ id: "claude-4.5-sonnet", contextWindow: CONTEXT_200K },
|
|
235
283
|
{ id: "claude-4.5-opus", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
236
|
-
{ id: "claude-4.6-opus", contextWindow:
|
|
237
|
-
{ id: "claude-4.6-sonnet", contextWindow:
|
|
238
|
-
{ id: "claude-opus-4-7", contextWindow:
|
|
239
|
-
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
{ id: "claude-opus-
|
|
245
|
-
{ id: "claude-
|
|
246
|
-
{ id: "claude-opus-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
247
|
-
{ id: "claude-opus-5-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
248
|
-
{ id: "claude-fable-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
284
|
+
{ id: "claude-4.6-opus", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
285
|
+
{ id: "claude-4.6-sonnet", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
286
|
+
{ id: "claude-opus-4-7", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
287
|
+
{ id: "claude-opus-4-8", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
288
|
+
// claude-opus-5: regular variant is quarantined (not_found on every Run) but
|
|
289
|
+
// the umbrella row routes the THINKING variant, which is live — so the base
|
|
290
|
+
// row returns to the seed under the umbrella (resolver never sends the
|
|
291
|
+
// quarantined regular wire id for the bare slug).
|
|
292
|
+
{ id: "claude-opus-5", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
293
|
+
{ id: "claude-fable-5", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
249
294
|
|
|
250
295
|
{ id: "composer-1", contextWindow: CONTEXT_200K },
|
|
251
296
|
{ id: "composer-2.5", contextWindow: CONTEXT_200K },
|
|
@@ -257,6 +302,10 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
257
302
|
{ id: "gemini-3-pro-image-preview", contextWindow: CONTEXT_200K },
|
|
258
303
|
{ id: "gemini-3.1-pro", contextWindow: CONTEXT_GEMINI },
|
|
259
304
|
{ id: "gemini-3.5-flash", contextWindow: CONTEXT_200K },
|
|
305
|
+
// 260825 live GetUsableModels: both ship only as effort-suffixed ids, so each exposes a tier
|
|
306
|
+
// picker. 3.6 is the only Cursor model with a `minimal` rung.
|
|
307
|
+
{ id: "gemini-3.6-flash", contextWindow: CONTEXT_GEMINI, supportsReasoningEffort: true },
|
|
308
|
+
{ id: "gemini-3.7-flash", contextWindow: CONTEXT_GEMINI, supportsReasoningEffort: true },
|
|
260
309
|
|
|
261
310
|
{ id: "gpt-5-codex", contextWindow: CONTEXT_272K },
|
|
262
311
|
{ id: "gpt-5-fast", contextWindow: CONTEXT_272K },
|
|
@@ -290,13 +339,15 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
290
339
|
{ id: "kimi-k2.7-code", contextWindow: CONTEXT_262K },
|
|
291
340
|
// kimi-k3: cursor.com/docs/models/kimi-k3; account-verified via GetUsableModels (2026-07-28) —
|
|
292
341
|
// ships only as effort-suffixed kimi-k3-{low,high,max}, so the tier picker is exposed.
|
|
293
|
-
|
|
342
|
+
// kimi-k3 folds the old synthetic kimi-k3-1m row into the umbrella: the base
|
|
343
|
+
// is maxModeVerified (user-verified 1M on the Ultra plan, devlog 260826/025),
|
|
344
|
+
// so the ultra effort rung arms Max Mode on the wire and the separate picker
|
|
345
|
+
// row is gone. cursor/kimi-k3-1m stays routable as an alias.
|
|
346
|
+
{ id: "kimi-k3", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
294
347
|
|
|
295
348
|
{ id: "grok-4.5", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
296
|
-
{ id: "grok-4.5-fast", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
297
349
|
// 260813 preemptive: grok-4.6 seeded ahead of Cursor's lineup update (mirrors grok-4.5).
|
|
298
350
|
{ id: "grok-4.6", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
299
|
-
{ id: "grok-4.6-fast", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
300
351
|
]);
|
|
301
352
|
|
|
302
353
|
export function cursorModelIds(models: readonly CursorModelInfo[] = CURSOR_STATIC_MODELS): string[] {
|
|
@@ -34,12 +34,33 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
34
34
|
"claude-opus-5-fast": ["low", "medium", "high"],
|
|
35
35
|
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
36
36
|
"glm-5.2": ["high", "max"],
|
|
37
|
+
// 260825 live GetUsableModels. gemini-3.6-flash is the only Cursor model exposing `minimal`;
|
|
38
|
+
// listing it here is also what admits the suffix into CANONICAL_EFFORT_SUFFIXES below.
|
|
39
|
+
"gemini-3.6-flash": ["minimal", "low", "medium", "high"],
|
|
40
|
+
"gemini-3.7-flash": ["low", "medium", "high"],
|
|
41
|
+
// Explicit-thinking variants (260825 live roster). Tiers are the rungs the wire actually
|
|
42
|
+
// lists for each family, which is not always the same set the non-thinking id carries:
|
|
43
|
+
// 4.6-opus thinks only at high/max, 4.5-opus only at high, 4.6-sonnet only at medium.
|
|
44
|
+
"claude-opus-5-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
45
|
+
"claude-opus-5-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
46
|
+
"claude-opus-4-8-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
47
|
+
"claude-opus-4-8-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
48
|
+
"claude-opus-4-7-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
49
|
+
"claude-opus-4-7-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
50
|
+
"claude-sonnet-5-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
51
|
+
"claude-fable-5-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
52
|
+
"claude-4.6-opus-thinking": ["high", "max"],
|
|
53
|
+
"claude-4.5-opus-thinking": ["high"],
|
|
54
|
+
"claude-4.6-sonnet-thinking": ["medium"],
|
|
37
55
|
// 260814 preemptive: glm-5.3 seeded ahead of Cursor's lineup update. Unlike 5.2, Z.AI folds
|
|
38
56
|
// 5.3 efforts into low/high/max (docs.z.ai/devpack/latest-model), so `low` is a real tier.
|
|
39
57
|
"glm-5.3": ["low", "high", "max"],
|
|
40
58
|
// GetUsableModels (2026-07-28) lists kimi-k3 only as effort-suffixed kimi-k3-{low,high,max};
|
|
41
59
|
// the bare id returns not_found. Tiers mirror the native Kimi provider's K3 ladder.
|
|
42
60
|
"kimi-k3": ["low", "high", "max"],
|
|
61
|
+
// Synthetic ultra picker variant (devlog 260826 070): same tier ladder as kimi-k3; the -1m
|
|
62
|
+
// marker is stripped before wire-id composition, so these tiers never form a wire suffix.
|
|
63
|
+
"kimi-k3-1m": ["low", "high", "max"],
|
|
43
64
|
// Cursor renamed the Grok 4.5 slugs to cursor-grok-4.5-{low,medium,high} and
|
|
44
65
|
// cursor-grok-4.5-{low,medium,high}-fast. The bare Fast id returns not_found.
|
|
45
66
|
"grok-4.5": ["low", "medium", "high"],
|
|
@@ -71,6 +92,46 @@ export const CANONICAL_EFFORT_SUFFIXES: ReadonlySet<string> = new Set([
|
|
|
71
92
|
|
|
72
93
|
const CANONICAL_CODEX_EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
73
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Cursor's explicit-thinking variants, exposed as first-class Codex model ids the same way the
|
|
97
|
+
* `-fast` families were.
|
|
98
|
+
*
|
|
99
|
+
* `source` is the id whose wire name the variant is built from; `order` is where Cursor puts the
|
|
100
|
+
* thinking marker relative to the effort rung. All three shapes exist in the live roster
|
|
101
|
+
* (GetUsableModels, 260825), and using the wrong one is rejected with ERROR_BAD_MODEL_NAME:
|
|
102
|
+
*
|
|
103
|
+
* thinking-then-effort claude-opus-5-thinking-high, claude-opus-5-thinking-high-fast
|
|
104
|
+
* effort-then-thinking claude-4.6-opus-high-thinking
|
|
105
|
+
* bare claude-4.5-sonnet-thinking (the model has no effort rung)
|
|
106
|
+
*/
|
|
107
|
+
const CURSOR_THINKING_FAMILIES: Readonly<Record<string, { source: string; order: "thinking-then-effort" | "effort-then-thinking" | "bare" }>> = {
|
|
108
|
+
"claude-opus-5-thinking": { source: "claude-opus-5", order: "thinking-then-effort" },
|
|
109
|
+
"claude-opus-5-thinking-fast": { source: "claude-opus-5-fast", order: "thinking-then-effort" },
|
|
110
|
+
"claude-opus-4-8-thinking": { source: "claude-opus-4-8", order: "thinking-then-effort" },
|
|
111
|
+
"claude-opus-4-8-thinking-fast": { source: "claude-opus-4-8-fast", order: "thinking-then-effort" },
|
|
112
|
+
"claude-opus-4-7-thinking": { source: "claude-opus-4-7", order: "thinking-then-effort" },
|
|
113
|
+
"claude-opus-4-7-thinking-fast": { source: "claude-opus-4-7-fast", order: "thinking-then-effort" },
|
|
114
|
+
"claude-sonnet-5-thinking": { source: "claude-sonnet-5", order: "thinking-then-effort" },
|
|
115
|
+
"claude-fable-5-thinking": { source: "claude-fable-5", order: "thinking-then-effort" },
|
|
116
|
+
"claude-4.6-opus-thinking": { source: "claude-4.6-opus", order: "effort-then-thinking" },
|
|
117
|
+
"claude-4.5-opus-thinking": { source: "claude-4.5-opus", order: "effort-then-thinking" },
|
|
118
|
+
"claude-4.6-sonnet-thinking": { source: "claude-4.6-sonnet", order: "effort-then-thinking" },
|
|
119
|
+
"claude-4.5-sonnet-thinking": { source: "claude-4.5-sonnet", order: "bare" },
|
|
120
|
+
"claude-4-sonnet-thinking": { source: "claude-4-sonnet", order: "bare" },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Codex-facing ids for Cursor's explicit-thinking variants. */
|
|
124
|
+
export const CURSOR_THINKING_MODEL_IDS = Object.keys(CURSOR_THINKING_FAMILIES);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Picker order, which is the canonical ladder plus the declared sentinels that rank below `low`.
|
|
128
|
+
*
|
|
129
|
+
* `cursorModelEffortLadder` filters against this, so a tier absent from it is silently dropped
|
|
130
|
+
* from the Codex picker even though `cursorEffortSuffix` would happily send it. That is what
|
|
131
|
+
* hid `gemini-3.6-flash-minimal`, the one Cursor model with a `minimal` rung.
|
|
132
|
+
*/
|
|
133
|
+
const CURSOR_PICKER_EFFORT_ORDER = ["minimal", ...CANONICAL_CODEX_EFFORT_ORDER] as const;
|
|
134
|
+
|
|
74
135
|
function normalizeRequestedEffort(reasoning: string | undefined): string | undefined {
|
|
75
136
|
const normalized = reasoning?.toLowerCase();
|
|
76
137
|
return normalized === "ultra" ? "max" : normalized;
|
|
@@ -119,7 +180,7 @@ export function cursorModelEffortLadder(baseModelId: string): string[] | undefin
|
|
|
119
180
|
const tiers = CURSOR_MODEL_EFFORT_TIERS[baseModelId];
|
|
120
181
|
if (!tiers || tiers.length === 0) return undefined;
|
|
121
182
|
const tierSet = new Set(tiers);
|
|
122
|
-
return
|
|
183
|
+
return CURSOR_PICKER_EFFORT_ORDER.filter(effort => tierSet.has(effort));
|
|
123
184
|
}
|
|
124
185
|
|
|
125
186
|
/** Base models known to carry a reasoning-effort suffix (everything else is sent bare). */
|
|
@@ -133,6 +194,23 @@ export function cursorModelHasEffortTiers(baseModelId: string): boolean {
|
|
|
133
194
|
* and send the base model plus requested_model parameters instead.
|
|
134
195
|
*/
|
|
135
196
|
export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string {
|
|
197
|
+
const thinking = CURSOR_THINKING_FAMILIES[baseModelId];
|
|
198
|
+
if (thinking) {
|
|
199
|
+
const { source, order } = thinking;
|
|
200
|
+
// Cursor writes the thinking marker on either side of the effort depending on family
|
|
201
|
+
// (measured against GetUsableModels, 260825):
|
|
202
|
+
// thinking-then-effort claude-opus-5-thinking-high, ...-thinking-high-fast
|
|
203
|
+
// effort-then-thinking claude-4.6-opus-high-thinking
|
|
204
|
+
// bare claude-4.5-sonnet-thinking (no effort rung at all)
|
|
205
|
+
// Sending the wrong order returns ERROR_BAD_MODEL_NAME, so this is not cosmetic.
|
|
206
|
+
if (order === "bare") return `${source}-thinking`;
|
|
207
|
+
if (order === "effort-then-thinking") return `${source}-${effortSuffix}-thinking`;
|
|
208
|
+
if (source.endsWith("-fast")) {
|
|
209
|
+
const stem = source.slice(0, -"-fast".length);
|
|
210
|
+
return `${stem}-thinking-${effortSuffix}-fast`;
|
|
211
|
+
}
|
|
212
|
+
return `${source}-thinking-${effortSuffix}`;
|
|
213
|
+
}
|
|
136
214
|
if (baseModelId.endsWith("-fast")) {
|
|
137
215
|
return `${baseModelId.slice(0, -"-fast".length)}-${effortSuffix}-fast`;
|
|
138
216
|
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External Cursor output quarantine (devlog 260826 gaps 10-11).
|
|
3
|
+
*
|
|
4
|
+
* Gap 10: flattened tool-result history can prime an external model to echo the
|
|
5
|
+
* "[Tool Result]" envelope as its own reply.
|
|
6
|
+
*
|
|
7
|
+
* Gap 11: a model can invent a blocked native-tool attempt in visible commentary
|
|
8
|
+
* even though the only real current-turn tool is the advertised Codex bridge.
|
|
9
|
+
*
|
|
10
|
+
* Both failures are observable only at the output boundary. The sniffers below
|
|
11
|
+
* hold a bounded prefix until it either diverges or proves the failure, allowing
|
|
12
|
+
* cursor.ts to retry before any invalid text reaches the client.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ECHO_MARKERS = ["[Tool Result]", "[Tool Error]", "[tool_result]"] as const;
|
|
16
|
+
const MAX_SNIFF_BYTES = 40;
|
|
17
|
+
/** Mid-stream observer: max leading whitespace on a line before matching disarms. */
|
|
18
|
+
const MAX_MIDSTREAM_LINE_INDENT = 128;
|
|
19
|
+
/** Mid-stream observer: post-marker window watched for call-id corruption. */
|
|
20
|
+
const MIDSTREAM_CORRUPTION_WINDOW = 512;
|
|
21
|
+
/** Mid-stream observer: cumulative scan cap (UTF-16 code units, checked between feeds). */
|
|
22
|
+
export const MAX_MIDSTREAM_SCAN_LENGTH = 512 * 1024;
|
|
23
|
+
/** Mid-stream observer: findings retained per turn. */
|
|
24
|
+
const MAX_MIDSTREAM_FINDINGS = 8;
|
|
25
|
+
const MAX_ROUTING_COMMENTARY_BYTES = 512;
|
|
26
|
+
/** Aggregate quarantine cap: past this, flush and disarm. */
|
|
27
|
+
const MAX_HOLD_BYTES = 8 * 1024;
|
|
28
|
+
const encoder = new TextEncoder();
|
|
29
|
+
|
|
30
|
+
export class CursorToolResultEchoError extends Error {
|
|
31
|
+
readonly code = "cursor_tool_result_echo";
|
|
32
|
+
constructor(marker: string) {
|
|
33
|
+
super(
|
|
34
|
+
"Cursor external model echoed the replayed tool-result envelope (\"" + marker
|
|
35
|
+
+ "\") instead of continuing the task.",
|
|
36
|
+
);
|
|
37
|
+
this.name = "CursorToolResultEchoError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class CursorRoutingCommentaryError extends Error {
|
|
42
|
+
readonly code = "cursor_routing_commentary_hallucination";
|
|
43
|
+
constructor() {
|
|
44
|
+
super("Cursor external model invented a blocked tool surface before any tool call occurred.");
|
|
45
|
+
this.name = "CursorRoutingCommentaryError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type EchoSnifferDecision =
|
|
50
|
+
| { kind: "hold" }
|
|
51
|
+
| { kind: "flush" }
|
|
52
|
+
| { kind: "echo"; marker: string };
|
|
53
|
+
|
|
54
|
+
export interface MidstreamEchoFinding {
|
|
55
|
+
marker: string;
|
|
56
|
+
/** UTF-16 offset of the marker's line start within the turn's full text. */
|
|
57
|
+
offset: number;
|
|
58
|
+
callIdCorrupt: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Diagnostic-only mid-stream envelope-echo observer (devlog 260828 F1/F2).
|
|
63
|
+
*
|
|
64
|
+
* The prefix sniffer only watches the first ~40 bytes of a turn, but live
|
|
65
|
+
* probing caught grok-4.6 echoing "[Tool Result]" envelope blocks in the
|
|
66
|
+
* MIDDLE of an agent message — after legitimate leading text — one of them
|
|
67
|
+
* carrying a whitespace-spliced call-id ("fc_x mar-y" instead of "fc_x-y").
|
|
68
|
+
* Deltas at that point have already reached the client, so this observer
|
|
69
|
+
* never throws and never withholds output: it records findings so the
|
|
70
|
+
* adapter can emit a structured diagnostic at turn end. Only fixed marker
|
|
71
|
+
* enums, numeric offsets, and corruption booleans are retained — never
|
|
72
|
+
* content bytes.
|
|
73
|
+
*/
|
|
74
|
+
export class CursorMidstreamEchoObserver {
|
|
75
|
+
private lineBuffer = "";
|
|
76
|
+
private lineStartOffset = 0;
|
|
77
|
+
private totalLength = 0;
|
|
78
|
+
private disarmed = false;
|
|
79
|
+
private lineDisarmed = false;
|
|
80
|
+
private corruptionWatch: { finding: MidstreamEchoFinding; remaining: number; window: string } | undefined;
|
|
81
|
+
private readonly recorded: MidstreamEchoFinding[] = [];
|
|
82
|
+
|
|
83
|
+
feed(textDelta: string): void {
|
|
84
|
+
if (this.disarmed && !this.corruptionWatch) return;
|
|
85
|
+
let index = 0;
|
|
86
|
+
while (index < textDelta.length) {
|
|
87
|
+
const newline = textDelta.indexOf("\n", index);
|
|
88
|
+
const segment = newline === -1 ? textDelta.slice(index) : textDelta.slice(index, newline);
|
|
89
|
+
if (this.corruptionWatch) this.watchCorruption(segment + (newline === -1 ? "" : "\n"));
|
|
90
|
+
if (!this.disarmed && !this.lineDisarmed && segment.length > 0) {
|
|
91
|
+
this.lineBuffer += segment;
|
|
92
|
+
if (this.lineBuffer.length > MAX_MIDSTREAM_LINE_INDENT + 32) {
|
|
93
|
+
// Bound per-line work: nothing beyond the indent cap + longest marker can match.
|
|
94
|
+
this.lineDisarmed = !this.lineMatchesPrefixSoFar();
|
|
95
|
+
this.lineBuffer = this.lineBuffer.slice(0, MAX_MIDSTREAM_LINE_INDENT + 32);
|
|
96
|
+
}
|
|
97
|
+
this.checkLine();
|
|
98
|
+
}
|
|
99
|
+
if (newline === -1) break;
|
|
100
|
+
this.lineBuffer = "";
|
|
101
|
+
this.lineDisarmed = false;
|
|
102
|
+
this.lineStartOffset = this.totalLength + newline + 1;
|
|
103
|
+
index = newline + 1;
|
|
104
|
+
}
|
|
105
|
+
this.totalLength += textDelta.length;
|
|
106
|
+
if (this.totalLength > MAX_MIDSTREAM_SCAN_LENGTH) this.disarmed = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
findings(): readonly MidstreamEchoFinding[] {
|
|
110
|
+
if (this.corruptionWatch) {
|
|
111
|
+
this.settleCorruption();
|
|
112
|
+
}
|
|
113
|
+
return this.recorded;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private lineMatchesPrefixSoFar(): boolean {
|
|
117
|
+
const probe = this.lineBuffer.replace(/^[ \t]*/, "");
|
|
118
|
+
return ECHO_MARKERS.some(marker => probe.startsWith(marker) || marker.startsWith(probe));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private checkLine(): void {
|
|
122
|
+
const indentMatch = /^[ \t]*/.exec(this.lineBuffer);
|
|
123
|
+
const indent = indentMatch ? indentMatch[0].length : 0;
|
|
124
|
+
if (indent > MAX_MIDSTREAM_LINE_INDENT) {
|
|
125
|
+
this.lineDisarmed = true;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const probe = this.lineBuffer.slice(indent);
|
|
129
|
+
for (const marker of ECHO_MARKERS) {
|
|
130
|
+
if (probe.startsWith(marker)) {
|
|
131
|
+
// The prefix sniffer owns the very start of the turn; only offsets past
|
|
132
|
+
// its window count as mid-stream.
|
|
133
|
+
if (this.lineStartOffset === 0) {
|
|
134
|
+
this.lineDisarmed = true;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const finding: MidstreamEchoFinding = {
|
|
138
|
+
marker,
|
|
139
|
+
offset: this.lineStartOffset,
|
|
140
|
+
callIdCorrupt: false,
|
|
141
|
+
};
|
|
142
|
+
this.corruptionWatch = { finding, remaining: MIDSTREAM_CORRUPTION_WINDOW, window: "" };
|
|
143
|
+
this.lineDisarmed = true;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!ECHO_MARKERS.some(marker => marker.startsWith(probe)) && probe.length > 0) {
|
|
148
|
+
this.lineDisarmed = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private watchCorruption(text: string): void {
|
|
153
|
+
const watch = this.corruptionWatch;
|
|
154
|
+
if (!watch) return;
|
|
155
|
+
const take = Math.min(watch.remaining, text.length);
|
|
156
|
+
watch.window += text.slice(0, take);
|
|
157
|
+
watch.remaining -= take;
|
|
158
|
+
if (watch.remaining <= 0) this.settleCorruption();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private settleCorruption(): void {
|
|
162
|
+
const watch = this.corruptionWatch;
|
|
163
|
+
if (!watch) return;
|
|
164
|
+
const window = watch.window;
|
|
165
|
+
watch.finding.callIdCorrupt =
|
|
166
|
+
/fc_[0-9a-f]+[ \t]+mar-/.test(window)
|
|
167
|
+
|| /call_id: \S+[ \t]+\S+_0\b/.test(window);
|
|
168
|
+
if (this.recorded.length < MAX_MIDSTREAM_FINDINGS) this.recorded.push(watch.finding);
|
|
169
|
+
// Window text is discarded here; only booleans/offsets survive.
|
|
170
|
+
this.corruptionWatch = undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Incremental envelope-prefix sniffer. Leading whitespace is tolerated so a
|
|
176
|
+
* marker copied after a newline is still caught.
|
|
177
|
+
*/
|
|
178
|
+
export class CursorEnvelopeEchoSniffer {
|
|
179
|
+
private buffered = "";
|
|
180
|
+
private byteCount = 0;
|
|
181
|
+
private done = false;
|
|
182
|
+
|
|
183
|
+
get settled(): boolean {
|
|
184
|
+
return this.done;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
feed(textDelta: string): EchoSnifferDecision {
|
|
188
|
+
if (this.done) return { kind: "flush" };
|
|
189
|
+
this.buffered += textDelta;
|
|
190
|
+
this.byteCount += encoder.encode(textDelta).byteLength;
|
|
191
|
+
const probe = this.buffered.replace(/^\s+/, "");
|
|
192
|
+
for (const marker of ECHO_MARKERS) {
|
|
193
|
+
if (probe.startsWith(marker)) {
|
|
194
|
+
this.done = true;
|
|
195
|
+
return { kind: "echo", marker };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const stillPrefix = ECHO_MARKERS.some(marker =>
|
|
199
|
+
probe.length < marker.length && marker.startsWith(probe),
|
|
200
|
+
);
|
|
201
|
+
if (stillPrefix && this.byteCount <= MAX_SNIFF_BYTES && this.buffered.length < MAX_HOLD_BYTES) {
|
|
202
|
+
return { kind: "hold" };
|
|
203
|
+
}
|
|
204
|
+
this.done = true;
|
|
205
|
+
return { kind: "flush" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
finish(): EchoSnifferDecision {
|
|
209
|
+
if (this.done) return { kind: "flush" };
|
|
210
|
+
this.done = true;
|
|
211
|
+
return { kind: "flush" };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type RoutingCommentaryDecision =
|
|
216
|
+
| { kind: "hold" }
|
|
217
|
+
| { kind: "flush" }
|
|
218
|
+
| { kind: "hallucination" };
|
|
219
|
+
|
|
220
|
+
const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b/giu;
|
|
221
|
+
const ROUTING_TOOL_HINT =
|
|
222
|
+
/(?:\b(?:shell|read|grep|list|bash)\b|exec_command|shell_command|브리지|네이티브\s*(?:셸|쉘))/iu;
|
|
223
|
+
const ROUTING_FAILURE_CLAIM =
|
|
224
|
+
/(?:blocked|unavailable|interrupted|차단|중단|막혀)/iu;
|
|
225
|
+
const ROUTING_REDIRECT_CLAIM =
|
|
226
|
+
/(?:exec_command|shell_command|\bexec\b|브리지|redirected|fallback|switch(?:ed|ing)?|전환|우회|통과(?:되|하)|다른\s*(?:도구|경로)|경로로)/iu;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Quarantines the first line of code-mode / bridge output long enough to reject
|
|
230
|
+
* an impossible routing claim. It requires a failure claim plus either an
|
|
231
|
+
* explicit redirect to another execution surface or two distinct unadvertised
|
|
232
|
+
* native-tool names; a legitimate sentence such as "Shell is unavailable on
|
|
233
|
+
* this OS" therefore passes.
|
|
234
|
+
*/
|
|
235
|
+
export class CursorRoutingCommentarySniffer {
|
|
236
|
+
private buffered = "";
|
|
237
|
+
private byteCount = 0;
|
|
238
|
+
private done = false;
|
|
239
|
+
|
|
240
|
+
get settled(): boolean {
|
|
241
|
+
return this.done;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
feed(textDelta: string): RoutingCommentaryDecision {
|
|
245
|
+
if (this.done) return { kind: "flush" };
|
|
246
|
+
this.buffered += textDelta;
|
|
247
|
+
this.byteCount += encoder.encode(textDelta).byteLength;
|
|
248
|
+
if (this.matchesHallucination()) {
|
|
249
|
+
this.done = true;
|
|
250
|
+
return { kind: "hallucination" };
|
|
251
|
+
}
|
|
252
|
+
const lineBreakCount = (this.buffered.match(/\n/gu) ?? []).length;
|
|
253
|
+
const hasRoutingHint = ROUTING_TOOL_HINT.test(this.buffered) || ROUTING_FAILURE_CLAIM.test(this.buffered);
|
|
254
|
+
const pendingFailureClaim =
|
|
255
|
+
ROUTING_TOOL_HINT.test(this.buffered)
|
|
256
|
+
&& ROUTING_FAILURE_CLAIM.test(this.buffered)
|
|
257
|
+
&& lineBreakCount < 2;
|
|
258
|
+
if (
|
|
259
|
+
this.byteCount < MAX_ROUTING_COMMENTARY_BYTES
|
|
260
|
+
&& this.buffered.length < MAX_HOLD_BYTES
|
|
261
|
+
&& (lineBreakCount === 0 || pendingFailureClaim)
|
|
262
|
+
&& (hasRoutingHint || this.byteCount < 64)
|
|
263
|
+
) {
|
|
264
|
+
return { kind: "hold" };
|
|
265
|
+
}
|
|
266
|
+
this.done = true;
|
|
267
|
+
return { kind: "flush" };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
finish(): RoutingCommentaryDecision {
|
|
271
|
+
if (this.done) return { kind: "flush" };
|
|
272
|
+
this.done = true;
|
|
273
|
+
return this.matchesHallucination() ? { kind: "hallucination" } : { kind: "flush" };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private matchesHallucination(): boolean {
|
|
277
|
+
if (!ROUTING_FAILURE_CLAIM.test(this.buffered)) return false;
|
|
278
|
+
const nativeTools = new Set(
|
|
279
|
+
[...this.buffered.matchAll(ROUTING_NATIVE_TOOL_NAME)].map(match => match[1]?.toLowerCase()),
|
|
280
|
+
);
|
|
281
|
+
if (nativeTools.size === 0) return false;
|
|
282
|
+
return ROUTING_REDIRECT_CLAIM.test(this.buffered) || nativeTools.size >= 2;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const CURSOR_ECHO_RETRY_CONTINUATION_TEXT =
|
|
287
|
+
"Your previous reply copied an internal tool-output record verbatim and was rejected. Continue the original task now: issue the next required tool call, or answer in your own words if no tool is needed.";
|
|
288
|
+
|
|
289
|
+
export const CURSOR_ROUTING_COMMENTARY_RETRY_TEXT =
|
|
290
|
+
"Your previous reply claimed an execution-surface failure that did not occur and was rejected. Use the current tool catalog as ground truth. Perform the requested operation through the advertised execution tool now, with no commentary before the tool call.";
|
|
@@ -43,7 +43,7 @@ export interface CursorUsableModelsOptions {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export type CursorUsableModelsResult =
|
|
46
|
-
| { ok: true; models: string[] }
|
|
46
|
+
| { ok: true; models: string[]; maxModeModels?: string[] }
|
|
47
47
|
| { ok: false; error: "auth" | "http" | "policy" | "transport" | "timeout" | "decode" | "empty" | "too_large"; detail?: string };
|
|
48
48
|
|
|
49
49
|
/** Test-only seam for management connectivity probes; production callers retain the HTTP/2 path. */
|
|
@@ -120,6 +120,7 @@ function decodeCursorUsableModels(bytes: Uint8Array): CursorUsableModelsResult {
|
|
|
120
120
|
// make stale configured ids such as `composer-2` look activated.
|
|
121
121
|
const ids: string[] = [];
|
|
122
122
|
const seenIds = new Set<string>();
|
|
123
|
+
const maxModeIds: string[] = [];
|
|
123
124
|
for (const model of response.models ?? []) {
|
|
124
125
|
const rawId = (model as { modelId?: string }).modelId;
|
|
125
126
|
if (typeof rawId !== "string") continue;
|
|
@@ -127,9 +128,13 @@ function decodeCursorUsableModels(bytes: Uint8Array): CursorUsableModelsResult {
|
|
|
127
128
|
if (!isValidModelDiscoveryModelId(id) || seenIds.has(id)) continue;
|
|
128
129
|
seenIds.add(id);
|
|
129
130
|
ids.push(id);
|
|
131
|
+
// Preserve Max-Mode capability for ultra/big-context auto-detection (devlog 260826 070).
|
|
132
|
+
if ((model as { maxMode?: boolean }).maxMode === true) maxModeIds.push(id);
|
|
130
133
|
if (ids.length >= CURSOR_MAX_DISCOVERED_MODELS) break;
|
|
131
134
|
}
|
|
132
|
-
return ids.length > 0
|
|
135
|
+
return ids.length > 0
|
|
136
|
+
? { ok: true, models: ids, ...(maxModeIds.length > 0 ? { maxModeModels: maxModeIds } : {}) }
|
|
137
|
+
: { ok: false, error: "empty" };
|
|
133
138
|
} catch {
|
|
134
139
|
return { ok: false, error: "decode", detail: "Invalid GetUsableModels protobuf response" };
|
|
135
140
|
}
|
|
@@ -415,6 +415,20 @@ export function finalizeAfterDrain(state: ReturnType<typeof createCursorProtobuf
|
|
|
415
415
|
export function clientToolFinalizeGraceMsForRequest(request: CursorRunRequest, baseGraceMs = CLIENT_TOOL_FINALIZE_GRACE_MS): number {
|
|
416
416
|
if (request.rawMessages?.at(-1)?.role === "toolResult") return baseGraceMs;
|
|
417
417
|
const text = activePromptText(request);
|
|
418
|
+
// Parallel-tool requests with several advertised tools get the expanded window regardless of
|
|
419
|
+
// prompt shape: external models (grok) assemble sibling calls serially over multiple frames,
|
|
420
|
+
// and the 50ms drain grace ended the turn after 1-2 of them (devlog 260826_cursor_responses_gap,
|
|
421
|
+
// live 10-parallel probe: calls=2 then calls=1).
|
|
422
|
+
if (request.parallelToolCalls === true && (request.tools?.length ?? 0) > 1) {
|
|
423
|
+
const advertised = request.tools?.length ?? 0;
|
|
424
|
+
return Math.max(
|
|
425
|
+
baseGraceMs,
|
|
426
|
+
Math.min(
|
|
427
|
+
GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS,
|
|
428
|
+
Math.max(GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS, advertised * GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS),
|
|
429
|
+
),
|
|
430
|
+
);
|
|
431
|
+
}
|
|
418
432
|
if (!cursorRequestHasShellAlias(request.tools) || !isGenericToolUseCountDemoPrompt(text)) return baseGraceMs;
|
|
419
433
|
const requestedCount = requestedCursorToolUseCount(text);
|
|
420
434
|
const expandedGraceMs = requestedCount
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AdapterEvent } from "../../types";
|
|
2
|
+
import { encodeCursorCallId } from "./call-id";
|
|
2
3
|
import { cursorExecResult } from "./exec-policy";
|
|
3
4
|
import type { CursorClientMessage, CursorServerMessage } from "./types";
|
|
4
5
|
import type { CursorKvStore } from "./kv-store";
|
|
@@ -18,7 +19,9 @@ export function mapCursorServerMessage(
|
|
|
18
19
|
case "thinking":
|
|
19
20
|
return [{ type: "thinking_delta", thinking: message.thinking }];
|
|
20
21
|
case "tool_call_start":
|
|
21
|
-
|
|
22
|
+
// Cursor composite ids can contain a literal newline; Responses call_ids
|
|
23
|
+
// must stay single-line (see call-id.ts).
|
|
24
|
+
return [{ type: "tool_call_start", id: encodeCursorCallId(message.id), name: message.name }];
|
|
22
25
|
case "tool_call_delta":
|
|
23
26
|
return [{ type: "tool_call_delta", arguments: message.arguments }];
|
|
24
27
|
case "tool_call_end":
|
|
@@ -170,10 +170,33 @@ function runExternalJson(command: string, payload: unknown, config: DesktopExecu
|
|
|
170
170
|
}
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
// A command that never reads stdin - `echo`, a script that exits on a bad flag,
|
|
174
|
+
// anything that fails before its first read - closes the pipe while we are still
|
|
175
|
+
// writing to it. The write then fails with EPIPE, and on Linux that surfaces as an
|
|
176
|
+
// ASYNCHRONOUS 'error' event on the stream rather than a throw, so the try/catch
|
|
177
|
+
// below never saw it and the rejection escaped as an unhandled stream error. On
|
|
178
|
+
// macOS the same command usually drains the small payload first, which is why this
|
|
179
|
+
// only ever went red on the Linux shard.
|
|
180
|
+
//
|
|
181
|
+
// EPIPE here is not a failure of the executor CONTRACT: the child's exit code and
|
|
182
|
+
// stdout are what decide the result, and both are handled in 'close' above. So the
|
|
183
|
+
// pipe error is swallowed deliberately and the outcome is left to the child, which
|
|
184
|
+
// is what makes "bad output maps to failure" reachable instead of exploding.
|
|
185
|
+
child.stdin.on("error", (err: NodeJS.ErrnoException) => {
|
|
186
|
+
if (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED") return;
|
|
187
|
+
if (settled) return;
|
|
188
|
+
settled = true;
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
reject(err);
|
|
191
|
+
});
|
|
173
192
|
try {
|
|
174
193
|
child.stdin.write(JSON.stringify(payload));
|
|
175
194
|
child.stdin.end();
|
|
176
195
|
} catch (err) {
|
|
196
|
+
// Kept for the synchronous half: a stream already destroyed when we reach this
|
|
197
|
+
// line throws immediately instead of emitting.
|
|
198
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
199
|
+
if (code === "EPIPE" || code === "ERR_STREAM_DESTROYED") return;
|
|
177
200
|
if (!settled) {
|
|
178
201
|
settled = true;
|
|
179
202
|
clearTimeout(timer);
|