Skip to content
Closed
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
56 changes: 28 additions & 28 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"license": "ISC",
"type": "module",
"devDependencies": {
"@openai/codex": "^0.114.0",
"@openai/codex": "^0.116.0",
"@types/node": "^24.10.1",
"mcp-hello-world": "^1.1.2",
"tsx": "^4.20.6",
Expand Down
38 changes: 26 additions & 12 deletions src/CodexAcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import type {AuthenticationLogoutResponse, AuthenticationStatusResponse} from ".
* Converts ACP requests into corresponding app-server operations.
*/
export class CodexAcpClient {

private readonly codexClient: CodexAppServerClient;
private readonly config: JsonObject;
private readonly modelProvider: string | null;
Expand Down Expand Up @@ -69,19 +68,27 @@ export class CodexAcpClient {
}

switch (authRequest.methodId) {
case "api-key":
case "api-key": {
if (!authRequest._meta || !authRequest._meta["api-key"]) throw RequestError.invalidRequest();
const loginCompletedVersion = this.codexClient.getAccountLoginCompletedVersion();
await this.codexClient.accountLogin({
type: "apiKey",
apiKey: authRequest._meta["api-key"].apiKey
});
break;
case "chat-gpt":
this.gatewayConfig = null;
const result = await this.codexClient.awaitLoginCompleted(loginCompletedVersion);
return result.success;
}
case "chat-gpt": {
const loginCompletedVersion = this.codexClient.getAccountLoginCompletedVersion();
const loginResponse = await this.codexClient.accountLogin({type: "chatgpt"});
if (loginResponse.type == "chatgpt") {
await open(loginResponse.authUrl);
}
break;
this.gatewayConfig = null;
const result = await this.codexClient.awaitLoginCompleted(loginCompletedVersion);
return result.success;
}
case "gateway":
if (!authRequest._meta) throw RequestError.invalidRequest();

Expand Down Expand Up @@ -111,9 +118,7 @@ export class CodexAcpClient {

// Reset the gateway config to null if another authentication method was used
this.gatewayConfig = null;

const result = await this.codexClient.awaitLoginCompleted()
return result.success;
return false;
}


Expand Down Expand Up @@ -154,8 +159,9 @@ export class CodexAcpClient {
}

async logout(): Promise<AuthenticationLogoutResponse> {
const accountUpdatedVersion = this.codexClient.getAccountUpdatedVersion();
await this.codexClient.accountLogout();
await this.codexClient.awaitAccountUpdated();
await this.codexClient.awaitAccountUpdated(accountUpdatedVersion);
return {};
}

Expand All @@ -178,6 +184,7 @@ export class CodexAcpClient {

async resumeSession(request: acp.ResumeSessionRequest): Promise<SessionMetadata> {
await this.refreshSkills(request.cwd, request._meta);
const mcpStartupVersion = this.codexClient.getMcpStartupCompleteVersion();

const response = await this.codexClient.threadResume({
approvalPolicy: null,
Expand All @@ -200,10 +207,12 @@ export class CodexAcpClient {
sessionId: request.sessionId,
currentModelId: currentModelId,
models: codexModels,
mcpStartupVersion,
}
}

async loadSession(request: acp.LoadSessionRequest): Promise<SessionMetadataWithThread> {
const mcpStartupVersion = this.codexClient.getMcpStartupCompleteVersion();
const response = await this.codexClient.threadResume({
approvalPolicy: null,
sandbox: null,
Expand All @@ -226,11 +235,13 @@ export class CodexAcpClient {
currentModelId: currentModelId,
models: codexModels,
thread: response.thread,
mcpStartupVersion,
};
}

async newSession(request: acp.NewSessionRequest): Promise<SessionMetadata> {
await this.refreshSkills(request.cwd, request._meta);
const mcpStartupVersion = this.codexClient.getMcpStartupCompleteVersion();

const response = await this.codexClient.threadStart({
config: this.createSessionConfig(request.cwd, request.mcpServers),
Expand All @@ -256,12 +267,13 @@ export class CodexAcpClient {
sessionId: response.thread.id,
currentModelId: currentModelId,
models: codexModels,
mcpStartupVersion,
};
}

async awaitMcpServers(): Promise<Array<string>>{
const response = await this.codexClient.awaitMcpStartup();
return response.ready;
async awaitMcpStartup(mcpStartupVersion: number): Promise<Array<string>> {
const startup = await this.codexClient.awaitMcpStartup(mcpStartupVersion);
return startup.ready;
}

private createSessionConfig(projectPath: string, mcpServers: Array<McpServer>): JsonObject {
Expand Down Expand Up @@ -390,6 +402,7 @@ export class CodexAcpClient {
"vscode",
"exec",
"appServer",
"custom",
"subAgent",
"subAgentReview",
"subAgentCompact",
Expand Down Expand Up @@ -523,6 +536,7 @@ export type SessionMetadata = {
sessionId: string,
currentModelId: string,
models: Model[],
mcpStartupVersion: number,
}

export type SessionMetadataWithThread = SessionMetadata & {
Expand Down
34 changes: 25 additions & 9 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {CodexApprovalHandler} from "./CodexApprovalHandler";
import {CodexAuthMethods, type CodexAuthRequest} from "./CodexAuthMethod";
import {CodexAcpClient, type SessionMetadata, type SessionMetadataWithThread} from "./CodexAcpClient";
import {ACPSessionConnection, type UpdateSessionEvent} from "./ACPSessionConnection";
import type {Account, CollabAgentToolCallStatus, Model, RateLimitSnapshot, Thread, ThreadItem, UserInput, ReasoningEffortOption} from "./app-server/v2";
import type {Account, CollabAgentToolCallStatus, Model, Thread, ThreadItem, UserInput, ReasoningEffortOption} from "./app-server/v2";
import type {RateLimitsMap} from "./RateLimitsMap";
import type {InputModality, ReasoningEffort} from "./app-server";
import {ModelId} from "./ModelId";
Expand Down Expand Up @@ -127,7 +127,6 @@ export class CodexAcpServer implements acp.Agent {

async getOrCreateSession(request: acp.NewSessionRequest | acp.ResumeSessionRequest): Promise<[SessionId, SessionModelState, SessionModeState]> {
await this.checkAuthorization();
const pendingMcpServers: Promise<Array<string>> = this.codexAcpClient.awaitMcpServers();

let sessionMetadata: SessionMetadata;
if ("sessionId" in request) {
Expand All @@ -139,9 +138,8 @@ export class CodexAcpServer implements acp.Agent {
}

const accountResponse = await this.runWithProcessCheck(() => this.codexAcpClient.getAccount());
const {sessionId, currentModelId, models} = sessionMetadata;
logger.log(`Waiting MCP servers to start...`)
const sessionMcpServers = await pendingMcpServers;
const {sessionId, currentModelId, models, mcpStartupVersion} = sessionMetadata;
const sessionMcpServers = await this.resolveSessionMcpServers(request.mcpServers ?? [], mcpStartupVersion, "sessionId" in request);
const currentModel = this.findCurrentModel(models, currentModelId);
const sessionState: SessionState = {
sessionId: sessionId,
Expand Down Expand Up @@ -332,17 +330,15 @@ export class CodexAcpServer implements acp.Agent {
thread: Thread;
}> {
await this.checkAuthorization();
const pendingMcpServers: Promise<Array<string>> = this.codexAcpClient.awaitMcpServers();

logger.log(`Load existing session: ${request.sessionId}...`);
const sessionMetadata: SessionMetadataWithThread = await this.runWithProcessCheck(() =>
this.codexAcpClient.loadSession(request)
);

const accountResponse = await this.runWithProcessCheck(() => this.codexAcpClient.getAccount());
const {sessionId, currentModelId, models, thread} = sessionMetadata;
logger.log("Waiting MCP servers to start...");
const sessionMcpServers = await pendingMcpServers;
const {sessionId, currentModelId, models, thread, mcpStartupVersion} = sessionMetadata;
const sessionMcpServers = await this.resolveSessionMcpServers(request.mcpServers ?? [], mcpStartupVersion, true);
const currentModel = this.findCurrentModel(models, currentModelId);
const sessionState: SessionState = {
sessionId: sessionId,
Expand Down Expand Up @@ -601,6 +597,22 @@ export class CodexAcpServer implements acp.Agent {
return sessionState;
}

private async resolveSessionMcpServers(
mcpServers: Array<acp.McpServer>,
mcpStartupVersion: number,
recoverFromStartup: boolean,
): Promise<Array<string>> {
const requestedServerNames = getRequestedMcpServerNames(mcpServers);
if (requestedServerNames.length > 0) {
return requestedServerNames;
}
if (!recoverFromStartup) {
return [];
}
logger.log("Recovering MCP servers from startup state...");
return await this.runWithProcessCheck(() => this.codexAcpClient.awaitMcpStartup(mcpStartupVersion));
}

async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
logger.log("Prompt received", {
sessionId: params.sessionId,
Expand Down Expand Up @@ -747,3 +759,7 @@ export class CodexAcpServer implements acp.Agent {
}
}
}

function getRequestedMcpServerNames(mcpServers: Array<acp.McpServer>): Array<string> {
return Array.from(new Set(mcpServers.map(server => server.name)));
}
Loading
Loading