mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-18 20:59:49 +00:00
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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user