fix(codex): align local transcript message projection

Use semantic events as the visible text source and preserve completed plans as ordered plan proposal cards.
This commit is contained in:
weishu
2026-07-12 14:33:37 +08:00
parent 942a1dfff8
commit 8782b8a110
6 changed files with 313 additions and 82 deletions
+52 -2
View File
@@ -5,13 +5,15 @@ import { codexLocal } from './codexLocal';
import type { ReasoningEffort } from './appServerTypes';
import { CodexSession } from './session';
import { createCodexSessionScanner, type CodexSessionScanner } from './utils/codexSessionScanner';
import { convertCodexEvent } from './utils/codexEventConverter';
import { convertCodexEvent, type CodexMessage } from './utils/codexEventConverter';
import { buildHapiMcpBridge } from './utils/buildHapiMcpBridge';
import { parseCodexCliOverrides, stripCodexCliOverrides } from './utils/codexCliOverrides';
import { buildCodexPermissionModeCliArgs } from './utils/permissionModeConfig';
import { BaseLocalLauncher } from '@/modules/common/launcher/BaseLocalLauncher';
import { createCodexTranscriptLocator, type CodexTranscriptLocator } from './utils/codexTranscriptLocator';
type ProposedPlanMessage = Extract<CodexMessage, { type: 'proposed_plan' }>;
export async function codexLocalLauncher(session: CodexSession): Promise<'switch' | 'exit'> {
const resumeSessionId = session.sessionId;
let primarySessionId = resumeSessionId;
@@ -21,6 +23,8 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
let shuttingDown = false;
let pendingScannerSetup: Promise<void> | null = null;
let transcriptLocator: CodexTranscriptLocator | null = null;
let scannerTranscriptPath: string | null = null;
const pendingPlansByTurnId = new Map<string, ProposedPlanMessage>();
const permissionMode = session.getPermissionMode();
const managedPermissionMode = permissionMode === 'read-only' || permissionMode === 'safe-yolo' || permissionMode === 'yolo'
? permissionMode
@@ -61,6 +65,38 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
return primarySessionId === null || primarySessionId === sessionId;
};
const sendProposedPlan = (message: ProposedPlanMessage): void => {
const callId = `codex-proposed-plan:${message.id}`;
session.sendAgentMessage({
type: 'tool-call',
name: 'ExitPlanMode',
callId,
input: { plan: message.plan },
id: message.id
});
session.sendAgentMessage({
type: 'tool-call-result',
callId,
output: null,
id: `${message.id}:result`
});
};
const flushPendingPlan = (turnId: string): void => {
const message = pendingPlansByTurnId.get(turnId);
if (!message) {
return;
}
pendingPlansByTurnId.delete(turnId);
sendProposedPlan(message);
};
const flushAllPendingPlans = (): void => {
for (const turnId of pendingPlansByTurnId.keys()) {
flushPendingPlan(turnId);
}
};
const bindPrimarySession = (sessionId: string, transcriptPath: string, allowSwitch = false): void => {
if (primarySessionId && primarySessionId !== sessionId && !allowSwitch) {
logger.debug(`[codex-local]: Ignoring non-primary SessionStart hook ${sessionId}; primary is ${primarySessionId}`);
@@ -83,7 +119,11 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
return;
}
if (scanner) {
if (scannerTranscriptPath !== transcriptPath) {
flushAllPendingPlans();
}
await scanner.setTranscriptPath(transcriptPath);
scannerTranscriptPath = transcriptPath;
return;
}
const createdScanner = await createCodexSessionScanner({
@@ -112,7 +152,15 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
session.notifyUserActivity();
}
if (converted?.message) {
session.sendAgentMessage(converted.message);
if (converted.message.type === 'proposed_plan') {
// Codex may complete the Plan item before emitting its final text preface.
pendingPlansByTurnId.set(converted.message.turnId, converted.message);
} else {
session.sendAgentMessage(converted.message);
}
}
if (converted?.finishedTurnId) {
flushPendingPlan(converted.finishedTurnId);
}
}
});
@@ -121,6 +169,7 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
return;
}
scanner = createdScanner;
scannerTranscriptPath = transcriptPath;
};
const handleTranscriptPath = (transcriptPath: string): Promise<void> => {
@@ -248,6 +297,7 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
if (activeScanner) {
await activeScanner.cleanup();
}
flushAllPendingPlans();
happyServer.stop();
if (!hookReady) {
logger.debug('[codex-local]: SessionStart hook did not provide transcript path before shutdown');