remove codex mcp backend

This commit is contained in:
weishu
2026-03-16 21:50:09 +08:00
parent 16829b7c78
commit 34f931ef59
14 changed files with 154 additions and 892 deletions
+120 -256
View File
@@ -1,20 +1,17 @@
import React from 'react';
import { randomUUID } from 'node:crypto';
import { CodexMcpClient } from './codexMcpClient';
import { CodexAppServerClient } from './codexAppServerClient';
import { CodexPermissionHandler } from './utils/permissionHandler';
import { ReasoningProcessor } from './utils/reasoningProcessor';
import { DiffProcessor } from './utils/diffProcessor';
import { logger } from '@/ui/logger';
import { CodexDisplay } from '@/ui/ink/CodexDisplay';
import type { CodexSessionConfig } from './types';
import { buildHapiMcpBridge } from './utils/buildHapiMcpBridge';
import { emitReadyIfIdle } from './utils/emitReadyIfIdle';
import type { CodexSession } from './session';
import type { EnhancedMode } from './loop';
import { hasCodexCliOverrides } from './utils/codexCliOverrides';
import { buildCodexStartConfig } from './utils/codexStartConfig';
import { AppServerEventConverter } from './utils/appServerEventConverter';
import { registerAppServerPermissionHandlers } from './utils/appServerPermissionAdapter';
import { buildThreadStartParams, buildTurnStartParams } from './utils/appServerConfig';
@@ -26,17 +23,11 @@ import {
} from '@/modules/common/remote/RemoteLauncherBase';
type HappyServer = Awaited<ReturnType<typeof buildHapiMcpBridge>>['server'];
function shouldUseAppServer(): boolean {
const useMcpServer = process.env.CODEX_USE_MCP_SERVER === '1';
return !useMcpServer;
}
type QueuedMessage = { message: string; mode: EnhancedMode; isolate: boolean; hash: string };
class CodexRemoteLauncher extends RemoteLauncherBase {
private readonly session: CodexSession;
private readonly useAppServer: boolean;
private readonly mcpClient: CodexMcpClient | null;
private readonly appServerClient: CodexAppServerClient | null;
private readonly appServerClient: CodexAppServerClient;
private permissionHandler: CodexPermissionHandler | null = null;
private reasoningProcessor: ReasoningProcessor | null = null;
private diffProcessor: DiffProcessor | null = null;
@@ -48,9 +39,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
constructor(session: CodexSession) {
super(process.env.DEBUG ? session.logPath : undefined);
this.session = session;
this.useAppServer = shouldUseAppServer();
this.mcpClient = this.useAppServer ? null : new CodexMcpClient();
this.appServerClient = this.useAppServer ? new CodexAppServerClient() : null;
this.appServerClient = new CodexAppServerClient();
}
protected createDisplay(context: RemoteLauncherDisplayContext): React.ReactElement {
@@ -60,20 +49,17 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
private async handleAbort(): Promise<void> {
logger.debug('[Codex] Abort requested - stopping current task');
try {
if (this.useAppServer && this.appServerClient) {
if (this.currentThreadId && this.currentTurnId) {
try {
await this.appServerClient.interruptTurn({
threadId: this.currentThreadId,
turnId: this.currentTurnId
});
} catch (error) {
logger.debug('[Codex] Error interrupting app-server turn:', error);
}
if (this.currentThreadId && this.currentTurnId) {
try {
await this.appServerClient.interruptTurn({
threadId: this.currentThreadId,
turnId: this.currentTurnId
});
} catch (error) {
logger.debug('[Codex] Error interrupting app-server turn:', error);
}
this.currentTurnId = null;
}
this.currentTurnId = null;
this.abortController.abort();
this.session.queue.reset();
@@ -128,10 +114,8 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
protected async runMainLoop(): Promise<void> {
const session = this.session;
const messageBuffer = this.messageBuffer;
const useAppServer = this.useAppServer;
const mcpClient = this.mcpClient;
const appServerClient = this.appServerClient;
const appServerEventConverter = useAppServer ? new AppServerEventConverter() : null;
const appServerEventConverter = new AppServerEventConverter();
const normalizeCommand = (value: unknown): string | undefined => {
if (typeof value === 'string') {
@@ -265,14 +249,13 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
if (turnId) {
this.currentTurnId = turnId;
allowAnonymousTerminalEvent = false;
} else if (useAppServer && !this.currentTurnId) {
} else if (!this.currentTurnId) {
allowAnonymousTerminalEvent = true;
}
}
if (isTerminalEvent) {
if (shouldIgnoreTerminalEvent({
useAppServer,
eventTurnId,
currentTurnId: this.currentTurnId,
turnInFlight,
@@ -289,16 +272,6 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
allowAnonymousTerminalEvent = false;
}
if (!useAppServer) {
logger.debug(`[Codex] MCP message: ${JSON.stringify(msg)}`);
if (msgType === 'event_msg' || msgType === 'response_item' || msgType === 'session_meta') {
const payload = asRecord(msg.payload);
const payloadType = asString(payload?.type);
logger.debug(`[Codex] MCP wrapper event type: ${msgType}${payloadType ? ` (payload=${payloadType})` : ''}`);
}
}
if (msgType === 'agent_message') {
const message = asString(msg.message);
if (message) {
@@ -324,29 +297,18 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
messageBuffer.addMessage('Starting task...', 'status');
} else if (msgType === 'task_complete') {
messageBuffer.addMessage('Task completed', 'status');
if (!useAppServer) {
sendReady();
}
} else if (msgType === 'turn_aborted') {
messageBuffer.addMessage('Turn aborted', 'status');
if (!useAppServer) {
sendReady();
}
} else if (msgType === 'task_failed') {
const error = asString(msg.error);
messageBuffer.addMessage(error ? `Task failed: ${error}` : 'Task failed', 'status');
if (!useAppServer) {
sendReady();
}
}
if (msgType === 'task_started') {
clearReadyAfterTurnTimer?.();
if (useAppServer) {
turnInFlight = true;
if (!eventTurnId && !this.currentTurnId) {
allowAnonymousTerminalEvent = true;
}
turnInFlight = true;
if (!eventTurnId && !this.currentTurnId) {
allowAnonymousTerminalEvent = true;
}
if (!session.thinking) {
logger.debug('thinking started');
@@ -354,24 +316,20 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
}
}
if (isTerminalEvent) {
if (useAppServer) {
turnInFlight = false;
allowAnonymousTerminalEvent = false;
}
turnInFlight = false;
allowAnonymousTerminalEvent = false;
if (session.thinking) {
logger.debug('thinking completed');
session.onThinkingChange(false);
}
diffProcessor.reset();
appServerEventConverter?.reset();
appServerEventConverter.reset();
}
if (useAppServer) {
if (isTerminalEvent && !turnInFlight) {
scheduleReadyAfterTurn?.();
} else if (readyAfterTurnTimer && msgType !== 'task_started') {
scheduleReadyAfterTurn?.();
}
if (isTerminalEvent && !turnInFlight) {
scheduleReadyAfterTurn?.();
} else if (readyAfterTurnTimer && msgType !== 'task_started') {
scheduleReadyAfterTurn?.();
}
if (msgType === 'agent_reasoning_section_break') {
@@ -535,26 +493,18 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
}
};
if (useAppServer && appServerClient && appServerEventConverter) {
registerAppServerPermissionHandlers({
client: appServerClient,
permissionHandler
});
registerAppServerPermissionHandlers({
client: appServerClient,
permissionHandler
});
appServerClient.setNotificationHandler((method, params) => {
const events = appServerEventConverter.handleNotification(method, params);
for (const event of events) {
const eventRecord = asRecord(event) ?? { type: undefined };
handleCodexEvent(eventRecord);
}
});
} else if (mcpClient) {
mcpClient.setPermissionHandler(permissionHandler);
mcpClient.setHandler((msg) => {
const eventRecord = asRecord(msg) ?? { type: undefined };
appServerClient.setNotificationHandler((method, params) => {
const events = appServerEventConverter.handleNotification(method, params);
for (const event of events) {
const eventRecord = asRecord(event) ?? { type: undefined };
handleCodexEvent(eventRecord);
});
}
}
});
const { server: happyServer, mcpServers } = await buildHapiMcpBridge(session.client);
this.happyServer = happyServer;
@@ -580,33 +530,19 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
session.sendSessionEvent({ type: 'ready' });
};
const syncSessionId = () => {
if (!mcpClient) return;
const clientSessionId = mcpClient.getSessionId();
if (clientSessionId && clientSessionId !== session.sessionId) {
session.onSessionFound(clientSessionId);
await appServerClient.connect();
await appServerClient.initialize({
clientInfo: {
name: 'hapi-codex-client',
version: '1.0.0'
},
capabilities: {
experimentalApi: true
}
};
});
if (useAppServer && appServerClient) {
await appServerClient.connect();
await appServerClient.initialize({
clientInfo: {
name: 'hapi-codex-client',
version: '1.0.0'
},
capabilities: {
experimentalApi: true
}
});
} else if (mcpClient) {
await mcpClient.connect();
}
let wasCreated = false;
let currentModeHash: string | null = null;
let pending: { message: string; mode: EnhancedMode; isolate: boolean; hash: string } | null = null;
let first = true;
let hasThread = false;
let pending: QueuedMessage | null = null;
clearReadyAfterTurnTimer = () => {
if (!readyAfterTurnTimer) {
@@ -632,7 +568,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
while (!this.shouldExit) {
logActiveHandles('loop-top');
let message: { message: string; mode: EnhancedMode; isolate: boolean; hash: string } | null = pending;
let message: QueuedMessage | null = pending;
pending = null;
if (!message) {
const waitSignal = this.abortController.signal;
@@ -652,178 +588,111 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
break;
}
if (!useAppServer && wasCreated && currentModeHash && message.hash !== currentModeHash) {
logger.debug('[Codex] Mode changed restarting Codex session');
messageBuffer.addMessage('═'.repeat(40), 'status');
messageBuffer.addMessage('Starting new Codex session (mode changed)...', 'status');
mcpClient?.clearSession();
wasCreated = false;
currentModeHash = null;
pending = message;
permissionHandler.reset();
reasoningProcessor.abort();
diffProcessor.reset();
session.onThinkingChange(false);
continue;
}
messageBuffer.addMessage(message.message, 'user');
currentModeHash = message.hash;
try {
if (!wasCreated) {
if (useAppServer && appServerClient) {
const threadParams = buildThreadStartParams({
mode: message.mode,
mcpServers,
cliOverrides: session.codexCliOverrides
});
if (!hasThread) {
const threadParams = buildThreadStartParams({
mode: message.mode,
mcpServers,
cliOverrides: session.codexCliOverrides
});
const resumeCandidate = session.sessionId;
let threadId: string | null = null;
const resumeCandidate = session.sessionId;
let threadId: string | null = null;
if (resumeCandidate) {
try {
const resumeResponse = await appServerClient.resumeThread({
threadId: resumeCandidate,
...threadParams
}, {
signal: this.abortController.signal
});
const resumeRecord = asRecord(resumeResponse);
const resumeThread = resumeRecord ? asRecord(resumeRecord.thread) : null;
threadId = asString(resumeThread?.id) ?? resumeCandidate;
applyResolvedModel(resumeRecord?.model);
logger.debug(`[Codex] Resumed app-server thread ${threadId}`);
} catch (error) {
logger.warn(`[Codex] Failed to resume app-server thread ${resumeCandidate}, starting new thread`, error);
}
}
if (!threadId) {
const threadResponse = await appServerClient.startThread(threadParams, {
if (resumeCandidate) {
try {
const resumeResponse = await appServerClient.resumeThread({
threadId: resumeCandidate,
...threadParams
}, {
signal: this.abortController.signal
});
const threadRecord = asRecord(threadResponse);
const thread = threadRecord ? asRecord(threadRecord.thread) : null;
threadId = asString(thread?.id);
applyResolvedModel(threadRecord?.model);
if (!threadId) {
throw new Error('app-server thread/start did not return thread.id');
}
const resumeRecord = asRecord(resumeResponse);
const resumeThread = resumeRecord ? asRecord(resumeRecord.thread) : null;
threadId = asString(resumeThread?.id) ?? resumeCandidate;
applyResolvedModel(resumeRecord?.model);
logger.debug(`[Codex] Resumed app-server thread ${threadId}`);
} catch (error) {
logger.warn(`[Codex] Failed to resume app-server thread ${resumeCandidate}, starting new thread`, error);
}
if (!threadId) {
throw new Error('app-server resume did not return thread.id');
}
this.currentThreadId = threadId;
session.onSessionFound(threadId);
const turnParams = buildTurnStartParams({
threadId,
message: message.message,
mode: {
...message.mode,
model: session.getModel() ?? message.mode.model
},
cliOverrides: session.codexCliOverrides
});
turnInFlight = true;
allowAnonymousTerminalEvent = false;
const turnResponse = await appServerClient.startTurn(turnParams, {
signal: this.abortController.signal
});
const turnRecord = asRecord(turnResponse);
const turn = turnRecord ? asRecord(turnRecord.turn) : null;
const turnId = asString(turn?.id);
if (turnId) {
this.currentTurnId = turnId;
} else if (!this.currentTurnId) {
allowAnonymousTerminalEvent = true;
}
} else if (mcpClient) {
const startConfig: CodexSessionConfig = buildCodexStartConfig({
message: message.message,
mode: message.mode,
first,
mcpServers,
cliOverrides: session.codexCliOverrides
});
await mcpClient.startSession(startConfig, { signal: this.abortController.signal });
syncSessionId();
}
wasCreated = true;
first = false;
} else if (useAppServer && appServerClient) {
if (!threadId) {
const threadResponse = await appServerClient.startThread(threadParams, {
signal: this.abortController.signal
});
const threadRecord = asRecord(threadResponse);
const thread = threadRecord ? asRecord(threadRecord.thread) : null;
threadId = asString(thread?.id);
applyResolvedModel(threadRecord?.model);
if (!threadId) {
throw new Error('app-server thread/start did not return thread.id');
}
}
if (!threadId) {
throw new Error('app-server resume did not return thread.id');
}
this.currentThreadId = threadId;
session.onSessionFound(threadId);
hasThread = true;
} else {
if (!this.currentThreadId) {
logger.debug('[Codex] Missing thread id; restarting app-server thread');
wasCreated = false;
hasThread = false;
pending = message;
continue;
}
}
const turnParams = buildTurnStartParams({
threadId: this.currentThreadId,
message: message.message,
mode: {
...message.mode,
model: session.getModel() ?? message.mode.model
},
cliOverrides: session.codexCliOverrides
});
turnInFlight = true;
allowAnonymousTerminalEvent = false;
const turnResponse = await appServerClient.startTurn(turnParams, {
signal: this.abortController.signal
});
const turnRecord = asRecord(turnResponse);
const turn = turnRecord ? asRecord(turnRecord.turn) : null;
const turnId = asString(turn?.id);
if (turnId) {
this.currentTurnId = turnId;
} else if (!this.currentTurnId) {
allowAnonymousTerminalEvent = true;
}
} else if (mcpClient) {
await mcpClient.continueSession(message.message, { signal: this.abortController.signal });
syncSessionId();
const turnParams = buildTurnStartParams({
threadId: this.currentThreadId,
message: message.message,
mode: {
...message.mode,
model: session.getModel() ?? message.mode.model
},
cliOverrides: session.codexCliOverrides
});
turnInFlight = true;
allowAnonymousTerminalEvent = false;
const turnResponse = await appServerClient.startTurn(turnParams, {
signal: this.abortController.signal
});
const turnRecord = asRecord(turnResponse);
const turn = turnRecord ? asRecord(turnRecord.turn) : null;
const turnId = asString(turn?.id);
if (turnId) {
this.currentTurnId = turnId;
} else if (!this.currentTurnId) {
allowAnonymousTerminalEvent = true;
}
} catch (error) {
logger.warn('Error in codex session:', error);
const isAbortError = error instanceof Error && error.name === 'AbortError';
if (useAppServer) {
turnInFlight = false;
allowAnonymousTerminalEvent = false;
this.currentTurnId = null;
}
turnInFlight = false;
allowAnonymousTerminalEvent = false;
this.currentTurnId = null;
if (isAbortError) {
messageBuffer.addMessage('Aborted by user', 'status');
session.sendSessionEvent({ type: 'message', message: 'Aborted by user' });
if (!useAppServer) {
wasCreated = false;
currentModeHash = null;
logger.debug('[Codex] Marked session as not created after abort for proper resume');
}
} else {
messageBuffer.addMessage('Process exited unexpectedly', 'status');
session.sendSessionEvent({ type: 'message', message: 'Process exited unexpectedly' });
if (useAppServer) {
this.currentTurnId = null;
this.currentThreadId = null;
wasCreated = false;
}
this.currentTurnId = null;
this.currentThreadId = null;
hasThread = false;
}
} finally {
const shouldFinalizeTurnState = !useAppServer || !turnInFlight;
if (shouldFinalizeTurnState) {
if (!turnInFlight) {
permissionHandler.reset();
reasoningProcessor.abort();
diffProcessor.reset();
appServerEventConverter?.reset();
appServerEventConverter.reset();
session.onThinkingChange(false);
clearReadyAfterTurnTimer?.();
emitReadyIfIdle({
@@ -841,12 +710,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
protected async cleanup(): Promise<void> {
logger.debug('[codex-remote]: cleanup start');
try {
if (this.appServerClient) {
await this.appServerClient.disconnect();
}
if (this.mcpClient) {
await this.mcpClient.disconnect();
}
await this.appServerClient.disconnect();
} catch (error) {
logger.debug('[codex-remote]: Error disconnecting client', error);
}