feat: implement unified local launch failure handling policy

This commit is contained in:
weishu
2025-12-26 16:11:58 +08:00
parent 685c846b91
commit eee7a249fa
9 changed files with 149 additions and 21 deletions
+10 -1
View File
@@ -4,6 +4,7 @@ import { CodexSession } from './session';
import { Future } from '@/utils/future';
import { createCodexSessionScanner } from './utils/codexSessionScanner';
import { convertCodexEvent } from './utils/codexEventConverter';
import { getLocalLaunchExitReason } from '@/agent/localLaunchPolicy';
export async function codexLocalLauncher(session: CodexSession): Promise<'switch' | 'exit'> {
const scanner = await createCodexSessionScanner({
@@ -93,8 +94,16 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
logger.debug('[codex-local]: launch error', error);
const message = error instanceof Error ? error.message : String(error);
session.sendSessionEvent({ type: 'message', message: `Local Codex process failed: ${message}` });
const failureExitReason = exitReason ?? getLocalLaunchExitReason({
startedBy: session.startedBy,
startingMode: session.startingMode
});
session.recordLocalLaunchFailure(message, failureExitReason);
if (!exitReason) {
exitReason = 'switch';
exitReason = failureExitReason;
}
if (failureExitReason === 'exit') {
logger.warn(`[codex-local]: Local Codex process failed: ${message}`);
}
break;
}
+6 -1
View File
@@ -17,6 +17,7 @@ export interface EnhancedMode {
interface LoopOptions {
path: string;
startingMode?: 'local' | 'remote';
startedBy?: 'daemon' | 'terminal';
onModeChange: (mode: 'local' | 'remote') => void;
messageQueue: MessageQueue2<EnhancedMode>;
session: ApiSessionClient;
@@ -28,6 +29,8 @@ interface LoopOptions {
export async function loop(opts: LoopOptions): Promise<void> {
const logPath = logger.getLogPath();
const startedBy = opts.startedBy ?? 'terminal';
const startingMode = opts.startingMode ?? 'local';
const session = new CodexSession({
api: opts.api,
client: opts.session,
@@ -36,7 +39,9 @@ export async function loop(opts: LoopOptions): Promise<void> {
logPath,
messageQueue: opts.messageQueue,
onModeChange: opts.onModeChange,
mode: opts.startingMode ?? 'local',
mode: startingMode,
startedBy,
startingMode,
codexArgs: opts.codexArgs,
codexCliOverrides: opts.codexCliOverrides
});
+24 -5
View File
@@ -27,8 +27,9 @@ export async function runCodex(opts: {
}): Promise<void> {
const workingDirectory = process.cwd();
const sessionTag = randomUUID();
const startedBy = opts.startedBy ?? 'terminal';
logger.debug(`[codex] Starting with options: startedBy=${opts.startedBy || 'terminal'}`);
logger.debug(`[codex] Starting with options: startedBy=${startedBy}`);
const api = await ApiClient.create();
@@ -59,9 +60,9 @@ export async function runCodex(opts: {
happyHomeDir: configuration.happyHomeDir,
happyLibDir: runtimePath(),
happyToolsDir: resolve(runtimePath(), 'tools', 'unpacked'),
startedFromDaemon: opts.startedBy === 'daemon',
startedFromDaemon: startedBy === 'daemon',
hostPid: process.pid,
startedBy: opts.startedBy || 'terminal',
startedBy,
lifecycleState: 'running',
lifecycleStateSince: Date.now(),
flavor: 'codex'
@@ -82,7 +83,7 @@ export async function runCodex(opts: {
logger.debug('[START] Failed to report to daemon (may not be running):', error);
}
const startingMode: 'local' | 'remote' = opts.startedBy === 'daemon' ? 'remote' : 'local';
const startingMode: 'local' | 'remote' = startedBy === 'daemon' ? 'remote' : 'local';
session.updateAgentState((currentState) => ({
...currentState,
@@ -134,6 +135,15 @@ export async function runCodex(opts: {
let cleanupStarted = false;
let exitCode = 0;
let archiveReason = 'User terminated';
const formatFailureReason = (message: string): string => {
const maxLength = 200;
if (message.length <= maxLength) {
return message;
}
return `${message.slice(0, maxLength)}...`;
};
const cleanup = async (code: number = exitCode) => {
if (cleanupStarted) {
@@ -152,7 +162,7 @@ export async function runCodex(opts: {
lifecycleState: 'archived',
lifecycleStateSince: Date.now(),
archivedBy: 'cli',
archiveReason: 'User terminated'
archiveReason
}));
session.sendSessionDeath();
@@ -173,12 +183,14 @@ export async function runCodex(opts: {
process.on('uncaughtException', (error) => {
logger.debug('[codex] Uncaught exception:', error);
exitCode = 1;
archiveReason = 'Session crashed';
cleanup(1);
});
process.on('unhandledRejection', (reason) => {
logger.debug('[codex] Unhandled rejection:', reason);
exitCode = 1;
archiveReason = 'Session crashed';
cleanup(1);
});
@@ -194,6 +206,7 @@ export async function runCodex(opts: {
session,
codexArgs: opts.codexArgs,
codexCliOverrides,
startedBy,
onModeChange: (newMode) => {
session.sendSessionEvent({ type: 'switch', mode: newMode });
session.updateAgentState((currentState) => ({
@@ -208,8 +221,14 @@ export async function runCodex(opts: {
} catch (error) {
loopError = error;
exitCode = 1;
archiveReason = 'Session crashed';
logger.debug('[codex] Loop error:', error);
} finally {
const localFailure = sessionWrapper?.localLaunchFailure;
if (localFailure?.exitReason === 'exit') {
exitCode = 1;
archiveReason = `Local launch failed: ${formatFailureReason(localFailure.message)}`;
}
await cleanup(loopError ? 1 : exitCode);
}
}
+17
View File
@@ -3,10 +3,19 @@ import { MessageQueue2 } from '@/utils/MessageQueue2';
import { AgentSessionBase } from '@/agent/sessionBase';
import type { EnhancedMode } from './loop';
import type { CodexCliOverrides } from './utils/codexCliOverrides';
import type { LocalLaunchExitReason } from '@/agent/localLaunchPolicy';
type LocalLaunchFailure = {
message: string;
exitReason: LocalLaunchExitReason;
};
export class CodexSession extends AgentSessionBase<EnhancedMode> {
readonly codexArgs?: string[];
readonly codexCliOverrides?: CodexCliOverrides;
readonly startedBy: 'daemon' | 'terminal';
readonly startingMode: 'local' | 'remote';
localLaunchFailure: LocalLaunchFailure | null = null;
constructor(opts: {
api: ApiClient;
@@ -17,6 +26,8 @@ export class CodexSession extends AgentSessionBase<EnhancedMode> {
messageQueue: MessageQueue2<EnhancedMode>;
onModeChange: (mode: 'local' | 'remote') => void;
mode?: 'local' | 'remote';
startedBy: 'daemon' | 'terminal';
startingMode: 'local' | 'remote';
codexArgs?: string[];
codexCliOverrides?: CodexCliOverrides;
}) {
@@ -39,8 +50,14 @@ export class CodexSession extends AgentSessionBase<EnhancedMode> {
this.codexArgs = opts.codexArgs;
this.codexCliOverrides = opts.codexCliOverrides;
this.startedBy = opts.startedBy;
this.startingMode = opts.startingMode;
}
recordLocalLaunchFailure = (message: string, exitReason: LocalLaunchExitReason): void => {
this.localLaunchFailure = { message, exitReason };
};
sendCodexMessage = (message: unknown): void => {
this.client.sendCodexMessage(message);
};