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
27 changes: 25 additions & 2 deletions apps/server/src/codexAppServerManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,15 +310,28 @@ describe("readCodexAccountSnapshot", () => {
});
});

it("keeps spark enabled for api key accounts", () => {
it("disables spark for api key accounts", () => {
expect(
readCodexAccountSnapshot({
type: "apiKey",
}),
).toEqual({
type: "apiKey",
planType: null,
sparkEnabled: true,
sparkEnabled: false,
});
});

it("disables spark for unknown chatgpt plans", () => {
expect(
readCodexAccountSnapshot({
type: "chatgpt",
email: "unknown@example.com",
}),
).toEqual({
type: "chatgpt",
planType: "unknown",
sparkEnabled: false,
});
});
});
Expand All @@ -343,6 +356,16 @@ describe("resolveCodexModelForAccount", () => {
}),
).toBe("gpt-5.3-codex-spark");
});

it("falls back from spark to default for api key auth", () => {
expect(
resolveCodexModelForAccount("gpt-5.3-codex-spark", {
type: "apiKey",
planType: null,
sparkEnabled: false,
}),
).toBe("gpt-5.3-codex");
});
});

describe("startSession", () => {
Expand Down
104 changes: 10 additions & 94 deletions apps/server/src/codexAppServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ import {
isCodexCliVersionSupported,
parseCodexCliVersion,
} from "./provider/codexCliVersion";
import {
readCodexAccountSnapshot,
resolveCodexModelForAccount,
type CodexAccountSnapshot,
} from "./provider/codexAccount";
import { buildCodexInitializeParams, killCodexChildProcess } from "./provider/codexAppServer";

export { buildCodexInitializeParams } from "./provider/codexAppServer";
export { readCodexAccountSnapshot, resolveCodexModelForAccount } from "./provider/codexAccount";

type PendingRequestKey = string;

Expand Down Expand Up @@ -96,23 +105,6 @@ interface JsonRpcNotification {
params?: unknown;
}

type CodexPlanType =
| "free"
| "go"
| "plus"
| "pro"
| "team"
| "business"
| "enterprise"
| "edu"
| "unknown";

interface CodexAccountSnapshot {
readonly type: "apiKey" | "chatgpt" | "unknown";
readonly planType: CodexPlanType | null;
readonly sparkEnabled: boolean;
}

export interface CodexAppServerSendTurnInput {
readonly threadId: ThreadId;
readonly input?: string;
Expand Down Expand Up @@ -162,50 +154,6 @@ const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [
"unknown thread",
"does not exist",
];
const CODEX_DEFAULT_MODEL = "gpt-5.3-codex";
const CODEX_SPARK_MODEL = "gpt-5.3-codex-spark";
const CODEX_SPARK_DISABLED_PLAN_TYPES = new Set<CodexPlanType>(["free", "go", "plus"]);

function asObject(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== "object") {
return undefined;
}
return value as Record<string, unknown>;
}

function asString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}

export function readCodexAccountSnapshot(response: unknown): CodexAccountSnapshot {
const record = asObject(response);
const account = asObject(record?.account) ?? record;
const accountType = asString(account?.type);

if (accountType === "apiKey") {
return {
type: "apiKey",
planType: null,
sparkEnabled: true,
};
}

if (accountType === "chatgpt") {
const planType = (account?.planType as CodexPlanType | null) ?? "unknown";
return {
type: "chatgpt",
planType,
sparkEnabled: !CODEX_SPARK_DISABLED_PLAN_TYPES.has(planType),
};
}

return {
type: "unknown",
planType: null,
sparkEnabled: true,
};
}

export const CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Plan Mode (Conversational)

You work in 3 phases, and you should *chat your way* to a great plan before finalizing it. A great plan is very detailed-intent- and implementation-wise-so that it can be handed to another engineer or agent to be implemented right away. It must be **decision complete**, where the implementer does not need to make any decisions.
Expand Down Expand Up @@ -358,32 +306,13 @@ function mapCodexRuntimeMode(runtimeMode: RuntimeMode): {
};
}

export function resolveCodexModelForAccount(
model: string | undefined,
account: CodexAccountSnapshot,
): string | undefined {
if (model !== CODEX_SPARK_MODEL || account.sparkEnabled) {
return model;
}

return CODEX_DEFAULT_MODEL;
}

/**
* On Windows with `shell: true`, `child.kill()` only terminates the `cmd.exe`
* wrapper, leaving the actual command running. Use `taskkill /T` to kill the
* entire process tree instead.
*/
function killChildTree(child: ChildProcessWithoutNullStreams): void {
if (process.platform === "win32" && child.pid !== undefined) {
try {
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
return;
} catch {
// fallback to direct kill
}
}
child.kill();
killCodexChildProcess(child);
}

export function normalizeCodexModelSlug(
Expand All @@ -402,19 +331,6 @@ export function normalizeCodexModelSlug(
return normalized;
}

export function buildCodexInitializeParams() {
return {
clientInfo: {
name: "t3code_desktop",
title: "T3 Code Desktop",
version: "0.1.0",
},
capabilities: {
experimentalApi: true,
},
} as const;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Session default sparkEnabled contradicts new account logic

Medium Severity

The default account in the session context initializes sparkEnabled: true for the "unknown" type, but readCodexAccountSnapshot now returns sparkEnabled: false for unknown accounts. When account/read fails during session startup, the fallback default keeps spark enabled, contradicting the new whitelist-only-for-pro policy. This lets users without a confirmed pro plan use the spark model if the account probe request fails.

Additional Locations (1)
Fix in Cursor Fix in Web

function buildCodexCollaborationMode(input: {
readonly interactionMode?: "default" | "plan";
readonly model?: string;
Expand Down
Loading
Loading