Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 158 additions & 8 deletions apps/server/src/provider/cursorSdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,27 @@ import { describe, expect, it } from "vite-plus/test";

const cursorSdkUrl = new URL("./cursorSdk.ts", import.meta.url).href;

// Loads cursorSdk.ts in a fresh process. @cursor/sdk is stubbed so the guard
// can be tested without the real package. The vitest worker already has its
// own unhandledRejection listener, which would hide the process-exit behavior.
// Loads cursorSdk.ts in a fresh process. The send probe uses the real SDK;
// shell guard probes stub it. The worker's own unhandledRejection listener
// would hide the process-exit behavior.
const probeProgram = `
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { registerHooks } from "node:module";
import { createRequire, registerHooks } from "node:module";
import { pathToFileURL } from "node:url";
import { spawn } from "node:child_process";

const sdkRequire = createRequire(createRequire(${JSON.stringify(cursorSdkUrl)}).resolve("@cursor/sdk"));
const protobufUrl = pathToFileURL(sdkRequire.resolve("@bufbuild/protobuf")).href;
const { proto3, ScalarType } = sdkRequire("@bufbuild/protobuf");
const mode = process.argv[1];
if (mode === "sdk-model-requests") {
process.on("uncaughtException", error => {
console.error(error.stack);
process.exit(1);
});
}
const dir = mkdtempSync(join(tmpdir(), "cursor-sdk-stub-"));
const stub = join(dir, "stub.cjs");
writeFileSync(
Expand All @@ -23,17 +33,110 @@ writeFileSync(
);
registerHooks({
resolve(specifier, context, next) {
if (specifier === "@cursor/sdk") {
if (specifier === "@cursor/sdk" && mode !== "sdk-model-requests") {
return { url: pathToFileURL(stub).href, shortCircuit: true };
}
if (specifier === "@bufbuild/protobuf") {
return { url: protobufUrl, shortCircuit: true };
}
return next(specifier, context);
},
});

const { isCursorShellSpawnFailure } = await import(${JSON.stringify(cursorSdkUrl)});
const mode = process.argv[1];
const { isCursorShellSpawnFailure, createAgentPlatform } = await import(${JSON.stringify(cursorSdkUrl)});
const missingCwd = join(tmpdir(), "t3-missing-cwd-" + process.pid);

if (mode === "sdk-model-requests") {
const { JsonlLocalAgentStore } = sdkRequire("@cursor/sdk");
const store = new JsonlLocalAgentStore(join(dir, "store"));
const platform = await createAgentPlatform({ localStore: store, workspaceRef: dir });
// Catalog validation is an external dependency; the SDK's send path and
// generated RequestedModel constructor stay real. Stop before transport.
platform.resolveLocalModelSelection = async selection => selection;
globalThis.fetch = async () => { throw new Error("Unexpected network request"); };
const requests = [];
const initPartial = proto3.util.initPartial;
const captured = new Error("model request captured before transport");
proto3.util.initPartial = function(source, target) {
initPartial.call(this, source, target);
if (target.getType().typeName !== "agent.v1.RequestedModel" || !target.modelId) return;
const decoded = target.getType().fromBinary(target.toBinary());
requests.push({ model: decoded.modelId, maxMode: decoded.maxMode,
context: decoded.parameters.find(parameter => parameter.id === "context")?.value });
throw captured;
};
for (const [id, context] of [
["grok-4.7", "500k"], ["grok-4.7", "256k"],
["claude-opus-5-5", "1m"], ["claude-opus-5-5", "300k"],
]) {
const agent = await platform.createAgent({
apiKey: "t3-offline-test-key", model: { id, params: [{ id: "context", value: context }] },
mode: "plan", tools: [], local: { cwd: dir, store, autoReview: false,
settingSources: [], sandboxOptions: { enabled: false }, enableAgentRetries: false },
});
try {
await agent.send("Reply with exactly: pong");
throw new Error("SDK did not construct a model request");
} catch (error) {
if (error !== captured && error.cause !== captured) throw error;
} finally {
await agent[Symbol.asyncDispose]();
}
}
console.log(JSON.stringify(requests));
process.exit(0);
}

if (mode === "model-requests") {
const Parameter = proto3.makeMessageType("agent.v1.ModelParameter", [
{ no: 1, name: "id", kind: "scalar", T: ScalarType.STRING },
{ no: 2, name: "value", kind: "scalar", T: ScalarType.STRING },
]);
const fields = [
{ no: 1, name: "model_id", kind: "scalar", T: ScalarType.STRING },
{ no: 2, name: "max_mode", kind: "scalar", T: ScalarType.BOOL },
{ no: 3, name: "parameters", kind: "message", T: Parameter, repeated: true },
];
const RequestedModel = proto3.makeMessageType("agent.v1.RequestedModel", fields);
const OtherMessage = proto3.makeMessageType("other.RequestedModel", fields);
const cases = [
{ modelId: "claude-opus-5-5", context: "1m" },
{ modelId: "grok-4.7", context: "500k" },
{ modelId: "gpt-5.6-sol", context: "1M" },
{ modelId: "claude-opus-5-5", context: "0.5m" },
{ modelId: "claude-opus-5-5", context: "300k" },
{ modelId: "grok-4.7", context: "256k" },
{ modelId: "gpt-5.6-sol", context: "272k" },
{ modelId: "claude-sonnet-4-6", context: "200k" },
{ modelId: "claude-opus-5-5", context: "0.3m" },
{ modelId: "default" },
{ modelId: "custom", context: "unknown" },
{ modelId: "claude-opus-5-5", context: "300k", maxMode: true },
];
const requests = cases.map(({ context, ...selection }) => {
const source = {
...selection,
parameters: [
{ id: "reasoning_effort", value: "high" },
...(context === undefined ? [] : [{ id: "context", value: context }]),
{ id: "fast", value: "true" },
],
};
const sourceBefore = JSON.stringify(source);
const request = new RequestedModel(source);
const decoded = RequestedModel.fromBinary(request.toBinary());
return {
maxMode: decoded.maxMode,
modelId: decoded.modelId,
parameters: decoded.parameters.map(({ id, value }) => ({ id, value })),
sourceUnchanged: sourceBefore === JSON.stringify(source),
};
});
const unrelated = new OtherMessage({ parameters: [{ id: "context", value: "1m" }] });
console.log(JSON.stringify({ requests, unrelatedMaxMode: unrelated.maxMode }));
process.exit(0);
}

if (mode === "predicate") {
const cursorShell = Object.assign(new Error("spawn /bin/zsh ENOENT"), {
code: "ENOENT",
Expand Down Expand Up @@ -139,7 +242,7 @@ function runProbe(mode: string): Promise<{ code: number | null; stdout: string;
reject(new Error(`probe ${mode} timed out`));
}, 5_000);
child.on("error", reject);
child.on("exit", (code) => {
child.on("close", (code) => {
clearTimeout(timer);
resolve({ code, stdout, stderr });
});
Expand All @@ -163,6 +266,53 @@ describe("isCursorShellSpawnFailure", () => {
});
});

describe("Cursor long-context requests", () => {
it("sets Max Mode in real SDK sends for the reported context tiers", async () => {
const result = await runProbe("sdk-model-requests");
expect(result.code, result.stderr.slice(-2_000)).toBe(0);
expect(JSON.parse(result.stdout)).toEqual([
{ model: "grok-4.7", context: "500k", maxMode: true },
{ model: "grok-4.7", context: "256k", maxMode: false },
{ model: "claude-opus-5-5", context: "1m", maxMode: true },
{ model: "claude-opus-5-5", context: "300k", maxMode: false },
]);
});

it("serializes Max Mode for long tiers and preserves standard requests", async () => {
const result = await runProbe("model-requests");
expect(result.code).toBe(0);
const { requests, unrelatedMaxMode } = JSON.parse(result.stdout);
expect(requests.map((request: { maxMode: boolean }) => request.maxMode)).toEqual([
true,
true,
true,
true,
false,
false,
false,
false,
false,
false,
false,
true,
]);
expect(requests[0]).toEqual({
maxMode: true,
modelId: "claude-opus-5-5",
parameters: [
{ id: "reasoning_effort", value: "high" },
{ id: "context", value: "1m" },
{ id: "fast", value: "true" },
],
sourceUnchanged: true,
});
expect(requests.every((request: { sourceUnchanged: boolean }) => request.sourceUnchanged)).toBe(
true,
);
expect(unrelatedMaxMode).toBe(false);
});
});

describe("Cursor shell spawn guard", () => {
it("keeps the process alive when Cursor's shell spawn rejects", async () => {
const result = await runProbe("cursor-shell");
Expand Down
36 changes: 36 additions & 0 deletions apps/server/src/provider/cursorSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,42 @@ installCursorShellSpawnGuard();
// Cursor's Webpack chunks and local helpers must stay beside the SDK entry.
// createRequire also loads that disk-backed package from a Node SEA executable.
const requireCursorSdk = NodeModule.createRequire(import.meta.url);

// The SDK omits RequestedModel.maxMode and exposes no way to set it. Cursor's
// current context tiers above 300K require Max Mode, as in its CLI. Patch only
// that message in the SDK's own disk-backed protobuf runtime, before loading
// the SDK. Remove this workaround when ModelSelection supports Max Mode.
// https://github.com/pingdotgg/t3code/issues/15788
const requireCursorSdkDependency = NodeModule.createRequire(
requireCursorSdk.resolve("@cursor/sdk"),
);
const { proto3 } = requireCursorSdkDependency("@bufbuild/protobuf") as {
proto3: {
util: {
initPartial: (
source: unknown,
target: {
getType: () => { typeName: string };
maxMode?: boolean;
parameters?: ReadonlyArray<{ id: string; value: string }>;
},
) => void;
};
};
};
const initPartial = proto3.util.initPartial;
proto3.util.initPartial = function (source, target) {
initPartial.call(this, source, target);
if (target.getType().typeName !== "agent.v1.RequestedModel" || target.maxMode) {
return;
}
const context = target.parameters?.find((parameter) => parameter.id === "context")?.value ?? "";
const tier = /^(\d+(?:\.\d+)?)([km])$/i.exec(context);
if (tier && Number(tier[1]) * (tier[2]?.toLowerCase() === "m" ? 1_000_000 : 1_000) > 300_000) {
target.maxMode = true;
}
};

export const {
Agent,
AuthenticationError,
Expand Down
11 changes: 11 additions & 0 deletions docs/fork-differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ Code: `apps/web/src/components/chat/ProviderAccountPicker.tsx` and
`apps/web/src/components/chat/providerAccountSelection.ts`. User guide:
[providers-codex.md](./user/providers-codex.md#switch-accounts-in-an-existing-thread).

## Cursor long-context tiers use Max Mode

Choosing a Cursor context tier above 300K (Grok 4.7 at 500K, or 1M on other models) turns on
Cursor's Max Mode for the request, as Cursor's own CLI does. Upstream sends these tiers without
Max Mode, so Grok 4.7 at 500K fails with "Invalid parameters for registry model" and 1M tiers
silently run at about 300K. Max Mode is billed per token on usage-based Cursor plans.

This is a port of upstream PR [#15884](https://github.com/pingdotgg/t3code/pull/15884) for
[#15788](https://github.com/pingdotgg/t3code/issues/15788). Remove this section when upstream merges
it. Code: `apps/server/src/provider/cursorSdk.ts`.

## Agents panel drilldowns

Upstream makes every subagent a child thread, lists a thread's subagents under **Lineage** in the
Expand Down