fix(codex): preserve transcript sync across mode switches

This commit is contained in:
weishu
2026-07-28 12:20:54 +08:00
parent f0c7d0b7e7
commit 92958890de
6 changed files with 118 additions and 9 deletions
+55 -4
View File
@@ -80,6 +80,7 @@ function createSessionStub(
let localLaunchFailure: { message: string; exitReason: 'switch' | 'exit' } | null = null;
let sessionId: string | null = null;
let transcriptPath: string | null = initialTranscriptPath;
let transcriptHistoryReplayPending = replayTranscriptHistoryOnStart;
let modelReasoningEffort: string | null = null;
const modelReasoningEffortUpdates: Array<string | null> = [];
const transcriptPathCallbacks: Array<(path: string) => void> = [];
@@ -96,7 +97,10 @@ function createSessionStub(
startedBy: 'terminal' as const,
startingMode: 'local' as const,
codexArgs,
replayTranscriptHistoryOnStart,
shouldReplayTranscriptHistory: () => transcriptHistoryReplayPending,
markTranscriptHistoryReplayConsumed: () => {
transcriptHistoryReplayPending = false;
},
client: {
isPending: () => pendingClient,
rpcHandlerManager: {
@@ -628,9 +632,9 @@ describe('codexLocalLauncher', () => {
}
});
it('replays existing transcript messages when importing a Codex thread into a new Hapi session', async () => {
it('replays imported transcript history only on the first local attachment', async () => {
const transcriptPath = join(tempDir, 'codex-import-transcript.jsonl');
const { session, agentMessages } = createSessionStub('default', undefined, '/tmp/worktree', null, true);
const { session, userMessages, agentMessages } = createSessionStub('default', undefined, '/tmp/worktree', null, true);
let releaseRunBarrier: (() => void) | undefined;
harness.runBarrier = new Promise((resolve) => {
releaseRunBarrier = resolve;
@@ -640,6 +644,7 @@ describe('codexLocalLauncher', () => {
transcriptPath,
[
JSON.stringify({ type: 'session_meta', payload: { id: 'codex-thread-import' } }),
JSON.stringify({ type: 'event_msg', payload: { type: 'user_message', message: 'old imported prompt' } }),
JSON.stringify({ type: 'event_msg', payload: { type: 'agent_message', message: 'old imported message' } })
].join('\n') + '\n'
);
@@ -652,14 +657,60 @@ describe('codexLocalLauncher', () => {
});
await wait(300);
await appendFile(
transcriptPath,
JSON.stringify({ type: 'event_msg', payload: { type: 'user_message', message: 'tail before switch' } }) + '\n'
);
if (releaseRunBarrier) {
releaseRunBarrier();
}
await launcherPromise;
expect(userMessages).toEqual(['old imported prompt', 'tail before switch']);
expect(agentMessages.filter((message) => (
message as { message?: string }
).message === 'old imported message')).toHaveLength(1);
let releaseSecondRunBarrier: (() => void) | undefined;
harness.runBarrier = new Promise((resolve) => {
releaseSecondRunBarrier = resolve;
});
const secondLauncherPromise = codexLocalLauncher(session as never);
await wait(50);
harness.sessionHookHandlers[1]?.('codex-thread-import', {
transcript_path: transcriptPath
});
await wait(300);
expect(userMessages).toEqual(['old imported prompt', 'tail before switch']);
expect(agentMessages.filter((message) => (
message as { message?: string }
).message === 'old imported message')).toHaveLength(1);
await appendFile(
transcriptPath,
[
JSON.stringify({ type: 'event_msg', payload: { type: 'user_message', message: 'new local prompt' } }),
JSON.stringify({ type: 'event_msg', payload: { type: 'agent_message', message: 'new local response' } })
].join('\n') + '\n'
);
await wait(700);
if (releaseSecondRunBarrier) {
releaseSecondRunBarrier();
}
await secondLauncherPromise;
expect(userMessages).toEqual(['old imported prompt', 'tail before switch', 'new local prompt']);
expect(agentMessages.filter((message) => (
message as { message?: string }
).message === 'old imported message')).toHaveLength(1);
expect(agentMessages).toContainEqual({
type: 'message',
message: 'old imported message',
message: 'new local response',
id: expect.any(String)
});
});