Files
hapi/cli/src/codex/runCodex.ts
T
2026-03-16 21:50:09 +08:00

190 lines
7.4 KiB
TypeScript

import { logger } from '@/ui/logger';
import { loop, type EnhancedMode, type PermissionMode } from './loop';
import { MessageQueue2 } from '@/utils/MessageQueue2';
import { hashObject } from '@/utils/deterministicJson';
import { registerKillSessionHandler } from '@/claude/registerKillSessionHandler';
import type { AgentState } from '@/api/types';
import type { CodexSession } from './session';
import { parseCodexCliOverrides } from './utils/codexCliOverrides';
import { bootstrapSession } from '@/agent/sessionFactory';
import { createModeChangeHandler, createRunnerLifecycle, setControlledByUser } from '@/agent/runnerLifecycle';
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol';
import { CodexCollaborationModeSchema, PermissionModeSchema } from '@hapi/protocol/schemas';
import { formatMessageWithAttachments } from '@/utils/attachmentFormatter';
export { emitReadyIfIdle } from './utils/emitReadyIfIdle';
export async function runCodex(opts: {
startedBy?: 'runner' | 'terminal';
codexArgs?: string[];
permissionMode?: PermissionMode;
resumeSessionId?: string;
model?: string;
}): Promise<void> {
const workingDirectory = process.cwd();
const startedBy = opts.startedBy ?? 'terminal';
logger.debug(`[codex] Starting with options: startedBy=${startedBy}`);
let state: AgentState = {
controlledByUser: false
};
const { api, session } = await bootstrapSession({
flavor: 'codex',
startedBy,
workingDirectory,
agentState: state,
model: opts.model
});
const startingMode: 'local' | 'remote' = startedBy === 'runner' ? 'remote' : 'local';
setControlledByUser(session, startingMode);
const messageQueue = new MessageQueue2<EnhancedMode>((mode) => hashObject({
permissionMode: mode.permissionMode,
model: mode.model,
collaborationMode: mode.collaborationMode
}));
const codexCliOverrides = parseCodexCliOverrides(opts.codexArgs);
const sessionWrapperRef: { current: CodexSession | null } = { current: null };
let currentPermissionMode: PermissionMode = opts.permissionMode ?? 'default';
let currentModel = opts.model;
let currentCollaborationMode: EnhancedMode['collaborationMode'] = 'default';
const lifecycle = createRunnerLifecycle({
session,
logTag: 'codex',
stopKeepAlive: () => sessionWrapperRef.current?.stopKeepAlive()
});
lifecycle.registerProcessHandlers();
registerKillSessionHandler(session.rpcHandlerManager, lifecycle.cleanupAndExit);
const syncSessionMode = () => {
const sessionInstance = sessionWrapperRef.current;
if (!sessionInstance) {
return;
}
const sessionModel = sessionInstance.getModel();
if (sessionModel !== undefined) {
currentModel = sessionModel ?? undefined;
}
sessionInstance.setPermissionMode(currentPermissionMode);
sessionInstance.setModel(currentModel ?? null);
sessionInstance.setCollaborationMode(currentCollaborationMode);
logger.debug(
`[Codex] Synced session config for keepalive: ` +
`permissionMode=${currentPermissionMode}, model=${currentModel ?? 'auto'}, collaborationMode=${currentCollaborationMode}`
);
};
session.onUserMessage((message) => {
const sessionPermissionMode = sessionWrapperRef.current?.getPermissionMode();
if (sessionPermissionMode && isPermissionModeAllowedForFlavor(sessionPermissionMode, 'codex')) {
currentPermissionMode = sessionPermissionMode as PermissionMode;
}
const sessionModel = sessionWrapperRef.current?.getModel();
if (sessionModel !== undefined) {
currentModel = sessionModel ?? undefined;
}
const sessionCollaborationMode = sessionWrapperRef.current?.getCollaborationMode();
if (sessionCollaborationMode) {
currentCollaborationMode = sessionCollaborationMode;
}
const messagePermissionMode = currentPermissionMode;
logger.debug(
`[Codex] User message received with permission mode: ${currentPermissionMode}, ` +
`model: ${currentModel ?? 'auto'}, collaborationMode: ${currentCollaborationMode}`
);
const enhancedMode: EnhancedMode = {
permissionMode: messagePermissionMode ?? 'default',
model: currentModel,
collaborationMode: currentCollaborationMode
};
const formattedText = formatMessageWithAttachments(message.content.text, message.content.attachments);
messageQueue.push(formattedText, enhancedMode);
});
const formatFailureReason = (message: string): string => {
const maxLength = 200;
if (message.length <= maxLength) {
return message;
}
return `${message.slice(0, maxLength)}...`;
};
const resolvePermissionMode = (value: unknown): PermissionMode => {
const parsed = PermissionModeSchema.safeParse(value);
if (!parsed.success || !isPermissionModeAllowedForFlavor(parsed.data, 'codex')) {
throw new Error('Invalid permission mode');
}
return parsed.data as PermissionMode;
};
const resolveCollaborationMode = (value: unknown): EnhancedMode['collaborationMode'] => {
if (value === null) {
return 'default';
}
const parsed = CodexCollaborationModeSchema.safeParse(value);
if (!parsed.success) {
throw new Error('Invalid collaboration mode');
}
return parsed.data;
};
session.rpcHandlerManager.registerHandler('set-session-config', async (payload: unknown) => {
if (!payload || typeof payload !== 'object') {
throw new Error('Invalid session config payload');
}
const config = payload as { permissionMode?: unknown; collaborationMode?: unknown };
if (config.permissionMode !== undefined) {
currentPermissionMode = resolvePermissionMode(config.permissionMode);
}
if (config.collaborationMode !== undefined) {
currentCollaborationMode = resolveCollaborationMode(config.collaborationMode);
}
syncSessionMode();
return { applied: { permissionMode: currentPermissionMode, collaborationMode: currentCollaborationMode } };
});
try {
await loop({
path: workingDirectory,
startingMode,
messageQueue,
api,
session,
codexArgs: opts.codexArgs,
codexCliOverrides,
startedBy,
permissionMode: currentPermissionMode,
model: currentModel,
collaborationMode: currentCollaborationMode,
resumeSessionId: opts.resumeSessionId,
onModeChange: createModeChangeHandler(session),
onSessionReady: (instance) => {
sessionWrapperRef.current = instance;
syncSessionMode();
}
});
} catch (error) {
lifecycle.markCrash(error);
logger.debug('[codex] Loop error:', error);
} finally {
const localFailure = sessionWrapperRef.current?.localLaunchFailure;
if (localFailure?.exitReason === 'exit') {
lifecycle.setExitCode(1);
lifecycle.setArchiveReason(`Local launch failed: ${formatFailureReason(localFailure.message)}`);
}
await lifecycle.cleanupAndExit();
}
}