mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-16 20:40:01 +00:00
feat(codex): use hook to obtain the session id and transcript path
This commit is contained in:
@@ -1,17 +1,23 @@
|
||||
import { logger } from '@/ui/logger';
|
||||
import { startHookServer } from '@/claude/utils/startHookServer';
|
||||
import { codexLocal } from './codexLocal';
|
||||
import type { ReasoningEffort } from './appServerTypes';
|
||||
import { CodexSession } from './session';
|
||||
import { createCodexSessionScanner } from './utils/codexSessionScanner';
|
||||
import { createCodexSessionScanner, type CodexSessionScanner } from './utils/codexSessionScanner';
|
||||
import { convertCodexEvent } from './utils/codexEventConverter';
|
||||
import { buildHapiMcpBridge } from './utils/buildHapiMcpBridge';
|
||||
import { stripCodexCliOverrides } from './utils/codexCliOverrides';
|
||||
import { buildCodexPermissionModeCliArgs } from './utils/permissionModeConfig';
|
||||
import { BaseLocalLauncher } from '@/modules/common/launcher/BaseLocalLauncher';
|
||||
|
||||
const SESSION_HOOK_TIMEOUT_MS = 10_000;
|
||||
|
||||
export async function codexLocalLauncher(session: CodexSession): Promise<'switch' | 'exit'> {
|
||||
const resumeSessionId = session.sessionId;
|
||||
let scanner: Awaited<ReturnType<typeof createCodexSessionScanner>> | null = null;
|
||||
let scanner: CodexSessionScanner | null = null;
|
||||
let hookReady = false;
|
||||
let shuttingDown = false;
|
||||
let pendingScannerSetup: Promise<void> | null = null;
|
||||
const permissionMode = session.getPermissionMode();
|
||||
const managedPermissionMode = permissionMode === 'read-only' || permissionMode === 'safe-yolo' || permissionMode === 'yolo'
|
||||
? permissionMode
|
||||
@@ -27,9 +33,99 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
|
||||
const { server: happyServer, mcpServers } = await buildHapiMcpBridge(session.client);
|
||||
logger.debug(`[codex-local]: Started hapi MCP bridge server at ${happyServer.url}`);
|
||||
|
||||
const hookServer = await startHookServer({
|
||||
onSessionHook: (sessionId, data) => {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
session.onSessionFound(sessionId);
|
||||
if (typeof data.transcript_path === 'string' && data.transcript_path.length > 0) {
|
||||
hookReady = true;
|
||||
session.onTranscriptPathFound(data.transcript_path);
|
||||
}
|
||||
}
|
||||
});
|
||||
logger.debug(`[codex-local]: Started Codex SessionStart hook server on port ${hookServer.port}`);
|
||||
|
||||
let hookTimeout: ReturnType<typeof setTimeout> | null = setTimeout(() => {
|
||||
hookTimeout = null;
|
||||
const message = `No Codex SessionStart hook transcript path received within ${SESSION_HOOK_TIMEOUT_MS}ms.`;
|
||||
logger.warn(`[codex-local]: ${message}`);
|
||||
session.sendSessionEvent({
|
||||
type: 'message',
|
||||
message: `${message} Keeping local Codex running; remote transcript sync may be unavailable for this launch.`
|
||||
});
|
||||
}, SESSION_HOOK_TIMEOUT_MS);
|
||||
|
||||
const clearHookTimeout = () => {
|
||||
if (hookTimeout) {
|
||||
clearTimeout(hookTimeout);
|
||||
hookTimeout = null;
|
||||
}
|
||||
};
|
||||
|
||||
const reportTranscriptSyncFailure = (transcriptPath: string, error: unknown): void => {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
const message = `Codex transcript sync failed for ${transcriptPath}: ${detail}`;
|
||||
logger.warn(`[codex-local]: ${message}`);
|
||||
session.sendSessionEvent({
|
||||
type: 'message',
|
||||
message: `${message} Keeping local Codex running; remote transcript sync is unavailable for this launch.`
|
||||
});
|
||||
};
|
||||
|
||||
const handleSessionFound = (sessionId: string) => {
|
||||
session.onSessionFound(sessionId);
|
||||
scanner?.onNewSession(sessionId);
|
||||
};
|
||||
|
||||
const processTranscriptPath = async (transcriptPath: string): Promise<void> => {
|
||||
hookReady = true;
|
||||
clearHookTimeout();
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
if (scanner) {
|
||||
await scanner.setTranscriptPath(transcriptPath);
|
||||
return;
|
||||
}
|
||||
const createdScanner = await createCodexSessionScanner({
|
||||
transcriptPath,
|
||||
onSessionId: (sessionId) => {
|
||||
session.onSessionFound(sessionId);
|
||||
},
|
||||
onEvent: (event) => {
|
||||
const converted = convertCodexEvent(event);
|
||||
if (converted?.sessionId) {
|
||||
session.onSessionFound(converted.sessionId);
|
||||
}
|
||||
if (converted?.userMessage) {
|
||||
session.sendUserMessage(converted.userMessage);
|
||||
}
|
||||
if (converted?.message) {
|
||||
session.sendAgentMessage(converted.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (shuttingDown) {
|
||||
await createdScanner.cleanup();
|
||||
return;
|
||||
}
|
||||
scanner = createdScanner;
|
||||
};
|
||||
|
||||
const handleTranscriptPath = (transcriptPath: string): Promise<void> => {
|
||||
const setupTask = (pendingScannerSetup ?? Promise.resolve()).then(() => processTranscriptPath(transcriptPath));
|
||||
const observedTask = setupTask.catch((error) => {
|
||||
if (!shuttingDown) {
|
||||
reportTranscriptSyncFailure(transcriptPath, error);
|
||||
}
|
||||
});
|
||||
pendingScannerSetup = observedTask.finally(() => {
|
||||
if (pendingScannerSetup === observedTask) {
|
||||
pendingScannerSetup = null;
|
||||
}
|
||||
});
|
||||
return pendingScannerSetup;
|
||||
};
|
||||
|
||||
const launcher = new BaseLocalLauncher({
|
||||
@@ -47,7 +143,11 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
|
||||
onSessionFound: handleSessionFound,
|
||||
abort: abortSignal,
|
||||
codexArgs,
|
||||
mcpServers
|
||||
mcpServers,
|
||||
sessionHook: {
|
||||
port: hookServer.port,
|
||||
token: hookServer.token
|
||||
}
|
||||
});
|
||||
},
|
||||
sendFailureMessage: (message) => {
|
||||
@@ -60,42 +160,30 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
|
||||
switchLogMessage: 'doSwitch'
|
||||
});
|
||||
|
||||
const handleSessionMatchFailed = (message: string) => {
|
||||
logger.warn(`[codex-local]: ${message}`);
|
||||
session.sendSessionEvent({
|
||||
type: 'message',
|
||||
message: `${message} Keeping local Codex running; remote transcript sync may be unavailable for this launch.`
|
||||
});
|
||||
session.resetTranscriptPath();
|
||||
const handleTranscriptPathCallback = (transcriptPath: string) => {
|
||||
void handleTranscriptPath(transcriptPath);
|
||||
};
|
||||
|
||||
scanner = await createCodexSessionScanner({
|
||||
sessionId: resumeSessionId,
|
||||
cwd: session.path,
|
||||
startupTimestampMs: Date.now(),
|
||||
onSessionMatchFailed: handleSessionMatchFailed,
|
||||
onSessionFound: (sessionId) => {
|
||||
session.onSessionFound(sessionId);
|
||||
},
|
||||
onEvent: (event) => {
|
||||
const converted = convertCodexEvent(event);
|
||||
if (converted?.sessionId) {
|
||||
session.onSessionFound(converted.sessionId);
|
||||
scanner?.onNewSession(converted.sessionId);
|
||||
}
|
||||
if (converted?.userMessage) {
|
||||
session.sendUserMessage(converted.userMessage);
|
||||
}
|
||||
if (converted?.message) {
|
||||
session.sendAgentMessage(converted.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
session.addTranscriptPathCallback(handleTranscriptPathCallback);
|
||||
|
||||
try {
|
||||
return await launcher.run();
|
||||
} finally {
|
||||
await scanner?.cleanup();
|
||||
shuttingDown = true;
|
||||
clearHookTimeout();
|
||||
session.removeTranscriptPathCallback(handleTranscriptPathCallback);
|
||||
hookServer.stop();
|
||||
if (pendingScannerSetup) {
|
||||
await pendingScannerSetup;
|
||||
}
|
||||
const activeScanner = scanner as CodexSessionScanner | null;
|
||||
if (activeScanner) {
|
||||
await activeScanner.cleanup();
|
||||
}
|
||||
happyServer.stop();
|
||||
if (!hookReady) {
|
||||
logger.debug('[codex-local]: SessionStart hook did not provide transcript path before shutdown');
|
||||
}
|
||||
logger.debug('[codex-local]: Stopped hapi MCP bridge server');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user