Skip to content
Open
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
8 changes: 4 additions & 4 deletions apps/server/src/codexAppServerManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import path from "node:path";
import { ApprovalRequestId, ThreadId } from "@t3tools/contracts";

import {
buildCodexInitializeParams,
CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS,
CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS,
CodexAppServerManager,
classifyCodexStderrLine,
isRecoverableThreadResumeError,
normalizeCodexModelSlug,
} from "./codexAppServerManager";
import {
buildCodexInitializeParams,
readCodexAccountSnapshot,
resolveCodexModelForAccount,
} from "./codexAppServerManager";
} from "./provider/codexAccount";

const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value);

Expand Down Expand Up @@ -250,7 +252,6 @@ describe("readCodexAccountSnapshot", () => {
expect(
readCodexAccountSnapshot({
type: "chatgpt",
email: "plus@example.com",
planType: "plus",
}),
).toEqual({
Expand All @@ -264,7 +265,6 @@ describe("readCodexAccountSnapshot", () => {
expect(
readCodexAccountSnapshot({
type: "chatgpt",
email: "pro@example.com",
planType: "pro",
}),
).toEqual({
Expand Down
91 changes: 6 additions & 85 deletions apps/server/src/codexAppServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ import {
isCodexCliVersionSupported,
parseCodexCliVersion,
} from "./provider/codexCliVersion";
import {
buildCodexInitializeParams,
readCodexAccountSnapshot,
resolveCodexModelForAccount,
type CodexAccountSnapshot,
} from "./provider/codexAccount";

type PendingRequestKey = string;

Expand Down Expand Up @@ -97,23 +103,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 +151,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,17 +303,6 @@ 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
Expand Down Expand Up @@ -402,19 +336,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;
}

function buildCodexCollaborationMode(input: {
readonly interactionMode?: "default" | "plan";
readonly model?: string;
Expand Down
43 changes: 24 additions & 19 deletions apps/server/src/persistence/NodeSqliteClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,34 @@ import { assert, it } from "@effect/vitest";
import { Effect } from "effect";
import * as SqlClient from "effect/unstable/sql/SqlClient";

import * as SqliteClient from "./NodeSqliteClient.ts";
const supportsNodeSqlite = typeof process.versions.bun !== "string";

const layer = it.layer(SqliteClient.layerMemory());
if (!supportsNodeSqlite) {
it.skip("NodeSqliteClient requires node:sqlite support", () => {});
} else {
const SqliteClient = await import("./NodeSqliteClient.ts");
const layer = it.layer(SqliteClient.layerMemory());

layer("NodeSqliteClient", (it) => {
it.effect("runs prepared queries and returns positional values", () =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
layer("NodeSqliteClient", (it) => {
it.effect("runs prepared queries and returns positional values", () =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;

yield* sql`CREATE TABLE entries(id INTEGER PRIMARY KEY, name TEXT NOT NULL)`;
yield* sql`INSERT INTO entries(name) VALUES (${"alpha"}), (${"beta"})`;
yield* sql`CREATE TABLE entries(id INTEGER PRIMARY KEY, name TEXT NOT NULL)`;
yield* sql`INSERT INTO entries(name) VALUES (${"alpha"}), (${"beta"})`;

const rows = yield* sql<{ readonly id: number; readonly name: string }>`
const rows = yield* sql<{ readonly id: number; readonly name: string }>`
SELECT id, name FROM entries ORDER BY id
`;
assert.equal(rows.length, 2);
assert.equal(rows[0]?.name, "alpha");
assert.equal(rows[1]?.name, "beta");
assert.equal(rows.length, 2);
assert.equal(rows[0]?.name, "alpha");
assert.equal(rows[1]?.name, "beta");

const values = yield* sql`SELECT id, name FROM entries ORDER BY id`.values;
assert.equal(values.length, 2);
assert.equal(values[0]?.[1], "alpha");
assert.equal(values[1]?.[1], "beta");
}),
);
});
const values = yield* sql`SELECT id, name FROM entries ORDER BY id`.values;
assert.equal(values.length, 2);
assert.equal(values[0]?.[1], "alpha");
assert.equal(values[1]?.[1], "beta");
}),
);
});
}
76 changes: 74 additions & 2 deletions apps/server/src/provider/Layers/ProviderHealth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
parseClaudeAuthStatusFromOutput,
readCodexConfigModelProvider,
} from "./ProviderHealth";
import { extractCodexAccountPlan } from "../codexAccount";

// ── Test helpers ────────────────────────────────────────────────────

Expand Down Expand Up @@ -112,12 +113,54 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => {
assert.strictEqual(status.status, "ready");
assert.strictEqual(status.available, true);
assert.strictEqual(status.authStatus, "authenticated");
assert.strictEqual(status.plan, "pro");
}).pipe(
Effect.provide(
mockSpawnerLayer((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 };
if (joined === "login status") {
return {
stdout: '{"authenticated":true,"account":{"planType":"pro"}}\n',
stderr: "",
code: 0,
};
}
throw new Error(`Unexpected args: ${joined}`);
}),
),
),
);

it.effect("reads the codex plan via app-server when login status omits it", () =>
Effect.gen(function* () {
yield* withTempCodexHome();
const status = yield* checkCodexProviderStatus;
assert.strictEqual(status.provider, "codex");
assert.strictEqual(status.status, "ready");
assert.strictEqual(status.available, true);
assert.strictEqual(status.authStatus, "authenticated");
assert.strictEqual(status.plan, "team");
}).pipe(
Effect.provide(
mockSpawnerLayer((args) => {
const joined = args.join(" ");
if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };
if (joined === "login status") {
return {
stdout: '{"authenticated":true}\n',
stderr: "",
code: 0,
};
}
if (joined === "app-server") {
return {
stdout:
'{"id":1,"result":{}}\n{"id":2,"result":{"account":{"type":"chatgpt","planType":"team"}}}\n',
stderr: "",
code: 0,
};
}
throw new Error(`Unexpected args: ${joined}`);
}),
),
Expand Down Expand Up @@ -332,6 +375,17 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => {
assert.strictEqual(parsed.authStatus, "unauthenticated");
});

it("extracts planType from JSON output", () => {
const parsed = parseAuthStatusFromOutput({
stdout: '{"authenticated":true,"account":{"planType":"team"}}\n',
stderr: "",
code: 0,
});
assert.strictEqual(parsed.status, "ready");
assert.strictEqual(parsed.authStatus, "authenticated");
assert.strictEqual(parsed.plan, "team");
});

it("JSON without auth marker is warning", () => {
const parsed = parseAuthStatusFromOutput({
stdout: '[{"ok":true}]\n',
Expand All @@ -343,6 +397,22 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => {
});
});

describe("extractCodexAccountPlan", () => {
it("extracts planType from account/read responses", () => {
assert.strictEqual(
extractCodexAccountPlan({
result: {
account: {
type: "chatgpt",
planType: "pro",
},
},
}),
"pro",
);
});
});

// ── readCodexConfigModelProvider tests ─────────────────────────────

describe("readCodexConfigModelProvider", () => {
Expand Down Expand Up @@ -609,12 +679,14 @@ it.layer(NodeServices.layer)("ProviderHealth", (it) => {

it("JSON with loggedIn=true is authenticated", () => {
const parsed = parseClaudeAuthStatusFromOutput({
stdout: '{"loggedIn":true,"authMethod":"claude.ai"}\n',
stdout:
'{"loggedIn":true,"authMethod":"claude.ai","email":"claude@example.com","subscriptionType":"pro"}\n',
stderr: "",
code: 0,
});
assert.strictEqual(parsed.status, "ready");
assert.strictEqual(parsed.authStatus, "authenticated");
assert.strictEqual(parsed.plan, "pro");
});

it("JSON with loggedIn=false is unauthenticated", () => {
Expand Down
Loading