mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-22 22:01:18 +00:00
fix(codex): preserve transcript final answer ordering
This commit is contained in:
@@ -5,7 +5,12 @@ import { codexLocal } from './codexLocal';
|
||||
import type { ReasoningEffort } from './appServerTypes';
|
||||
import { CodexSession } from './session';
|
||||
import { createCodexSessionScanner, type CodexSessionScanner } from './utils/codexSessionScanner';
|
||||
import { createCodexEventConverter, type CodexMessage, type CodexSessionEvent } from './utils/codexEventConverter';
|
||||
import {
|
||||
createCodexEventConverter,
|
||||
type CodexConversionAction,
|
||||
type CodexMessage,
|
||||
type CodexSessionEvent
|
||||
} from './utils/codexEventConverter';
|
||||
import { buildHapiMcpBridge } from './utils/buildHapiMcpBridge';
|
||||
import { parseCodexCliOverrides, stripCodexCliOverrides } from './utils/codexCliOverrides';
|
||||
import { buildCodexPermissionModeCliArgs } from './utils/permissionModeConfig';
|
||||
@@ -89,21 +94,6 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
|
||||
});
|
||||
};
|
||||
|
||||
const drainAndCleanupScanner = async (
|
||||
activeScanner: CodexSessionScanner,
|
||||
replayedExistingHistory: boolean
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// Codex can flush its final transcript records after the last watcher tick.
|
||||
await activeScanner.flush();
|
||||
if (replayedExistingHistory) {
|
||||
session.markTranscriptHistoryReplayConsumed();
|
||||
}
|
||||
} finally {
|
||||
await activeScanner.cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSessionFound = (sessionId: string, allowSwitch = false): void => {
|
||||
if (primarySessionId && primarySessionId !== sessionId && !allowSwitch) {
|
||||
logger.debug(`[codex-local]: Ignoring non-primary Codex session id ${sessionId}; primary is ${primarySessionId}`);
|
||||
@@ -173,6 +163,100 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
|
||||
pendingExecWrappers.clear();
|
||||
};
|
||||
|
||||
const dispatchTranscriptActions = (
|
||||
actions: CodexConversionAction[],
|
||||
context: { replayedHistory: boolean }
|
||||
): void => {
|
||||
for (const action of actions) {
|
||||
if (action.type === 'session-found') {
|
||||
if (!isPrimarySessionId(action.sessionId)) {
|
||||
logger.debug(`[codex-local]: Ignoring converted session id ${action.sessionId}; primary is ${primarySessionId}`);
|
||||
return;
|
||||
}
|
||||
session.onSessionFound(action.sessionId);
|
||||
continue;
|
||||
}
|
||||
if (action.type === 'user-message') {
|
||||
session.sendUserMessage(action.message);
|
||||
continue;
|
||||
}
|
||||
if (action.type === 'user-activity') {
|
||||
session.notifyUserActivity();
|
||||
continue;
|
||||
}
|
||||
if (action.type === 'turn-finished') {
|
||||
flushPendingPlan(action.turnId);
|
||||
for (const message of toolHookBridge.finishTurn(action.turnId)) {
|
||||
session.sendAgentMessage(message);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const message = action.message;
|
||||
if (message.type === 'proposed_plan') {
|
||||
// Codex may complete the Plan item before emitting its final text preface.
|
||||
pendingPlansByTurnId.set(message.turnId, message);
|
||||
} else if (message.type === 'tool-call' && message.name === 'exec') {
|
||||
if (countHookCoveredExecCalls(message.input) === null) {
|
||||
session.sendAgentMessage(message);
|
||||
} else {
|
||||
pendingExecWrappers.set(message.callId, {
|
||||
message,
|
||||
...(action.turnId ? { turnId: action.turnId } : {})
|
||||
});
|
||||
}
|
||||
} else if (message.type === 'tool-call-result' && pendingExecWrappers.has(message.callId)) {
|
||||
const pending = pendingExecWrappers.get(message.callId);
|
||||
const turnId = pending?.turnId ?? action.turnId;
|
||||
if (pending && toolHookBridge.hasCompletedAllObservedNestedTools(turnId)) {
|
||||
pendingExecWrappers.delete(message.callId);
|
||||
} else {
|
||||
flushPendingExecWrapper(message.callId, message);
|
||||
}
|
||||
} else {
|
||||
const scopedMessage = message.type !== 'token_count'
|
||||
? message
|
||||
: context.replayedHistory
|
||||
? { ...message, model: transcriptModel, hapiUsageScope: 'imported-history' }
|
||||
: primarySessionId
|
||||
? {
|
||||
...message,
|
||||
model: transcriptModel,
|
||||
threadId: primarySessionId,
|
||||
thread_id: primarySessionId,
|
||||
hapiUsageScope: 'managed'
|
||||
}
|
||||
: { ...message, model: transcriptModel };
|
||||
session.sendAgentMessage(scopedMessage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const finalizeTranscriptConversion = (replayedExistingHistory: boolean): void => {
|
||||
dispatchTranscriptActions(convertTranscriptEvent.finalize(), {
|
||||
replayedHistory: replayedExistingHistory
|
||||
});
|
||||
};
|
||||
|
||||
const drainAndCleanupScanner = async (
|
||||
activeScanner: CodexSessionScanner,
|
||||
replayedExistingHistory: boolean
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// Codex can flush its final transcript records after the last watcher tick.
|
||||
await activeScanner.flush();
|
||||
if (replayedExistingHistory) {
|
||||
session.markTranscriptHistoryReplayConsumed();
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await activeScanner.cleanup();
|
||||
} finally {
|
||||
finalizeTranscriptConversion(replayedExistingHistory);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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}`);
|
||||
@@ -196,6 +280,8 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
|
||||
}
|
||||
if (scanner) {
|
||||
if (scannerTranscriptPath !== transcriptPath) {
|
||||
await scanner.flush();
|
||||
finalizeTranscriptConversion(scannerReplayedExistingHistory);
|
||||
flushAllPendingPlans();
|
||||
convertTranscriptEvent = createCodexEventConverter();
|
||||
}
|
||||
@@ -208,6 +294,9 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
|
||||
transcriptPath,
|
||||
// 中文注释:导入模式下允许 scanner 首次回放 transcript 全量内容,补齐 Codex 客户端里已有但 Hapi 还未看到的消息。
|
||||
replayExistingHistory,
|
||||
onReplayComplete: () => {
|
||||
finalizeTranscriptConversion(true);
|
||||
},
|
||||
onSessionId: (sessionId) => {
|
||||
if (!isPrimarySessionId(sessionId)) {
|
||||
logger.debug(`[codex-local]: Ignoring transcript session id ${sessionId}; primary is ${primarySessionId}`);
|
||||
@@ -224,63 +313,7 @@ export async function codexLocalLauncher(session: CodexSession): Promise<'switch
|
||||
if (observedReasoningEffort !== undefined) {
|
||||
session.setModelReasoningEffort(observedReasoningEffort);
|
||||
}
|
||||
const converted = convertTranscriptEvent(event);
|
||||
if (converted?.sessionId) {
|
||||
if (!isPrimarySessionId(converted.sessionId)) {
|
||||
logger.debug(`[codex-local]: Ignoring converted session id ${converted.sessionId}; primary is ${primarySessionId}`);
|
||||
return;
|
||||
}
|
||||
session.onSessionFound(converted.sessionId);
|
||||
}
|
||||
if (converted?.userMessage) {
|
||||
session.sendUserMessage(converted.userMessage);
|
||||
} else if (converted?.userActivity) {
|
||||
session.notifyUserActivity();
|
||||
}
|
||||
for (const message of converted?.messages ?? []) {
|
||||
if (message.type === 'proposed_plan') {
|
||||
// Codex may complete the Plan item before emitting its final text preface.
|
||||
pendingPlansByTurnId.set(message.turnId, message);
|
||||
} else if (message.type === 'tool-call' && message.name === 'exec') {
|
||||
if (countHookCoveredExecCalls(message.input) === null) {
|
||||
session.sendAgentMessage(message);
|
||||
} else {
|
||||
pendingExecWrappers.set(message.callId, {
|
||||
message,
|
||||
...(converted?.turnId ? { turnId: converted.turnId } : {})
|
||||
});
|
||||
}
|
||||
} else if (message.type === 'tool-call-result' && pendingExecWrappers.has(message.callId)) {
|
||||
const pending = pendingExecWrappers.get(message.callId);
|
||||
const turnId = pending?.turnId ?? converted?.turnId;
|
||||
if (pending && toolHookBridge.hasCompletedAllObservedNestedTools(turnId)) {
|
||||
pendingExecWrappers.delete(message.callId);
|
||||
} else {
|
||||
flushPendingExecWrapper(message.callId, message);
|
||||
}
|
||||
} else {
|
||||
const scopedMessage = message.type !== 'token_count'
|
||||
? message
|
||||
: context.replayedHistory
|
||||
? { ...message, model: transcriptModel, hapiUsageScope: 'imported-history' }
|
||||
: primarySessionId
|
||||
? {
|
||||
...message,
|
||||
model: transcriptModel,
|
||||
threadId: primarySessionId,
|
||||
thread_id: primarySessionId,
|
||||
hapiUsageScope: 'managed'
|
||||
}
|
||||
: { ...message, model: transcriptModel };
|
||||
session.sendAgentMessage(scopedMessage);
|
||||
}
|
||||
}
|
||||
if (converted?.finishedTurnId) {
|
||||
flushPendingPlan(converted.finishedTurnId);
|
||||
for (const message of toolHookBridge.finishTurn(converted.finishedTurnId)) {
|
||||
session.sendAgentMessage(message);
|
||||
}
|
||||
}
|
||||
dispatchTranscriptActions(convertTranscriptEvent(event), context);
|
||||
}
|
||||
});
|
||||
if (shuttingDown) {
|
||||
|
||||
Reference in New Issue
Block a user