修复 Codex rollout 历史合并并重新打包

This commit is contained in:
shiyue
2026-09-20 19:34:30 +08:00
parent 9242e79520
commit db04e088bc
11 changed files with 615 additions and 49 deletions

View File

@@ -4580,6 +4580,14 @@ function assertHistoryRecallContract() {
/const existingIndexes = new Set\([\s\S]*?data-message-index/.test(frontendSource),
'Frontend should deduplicate history messages by stable message index'
);
assert(frontendSource.includes('function getStableMessageKey'), 'Frontend should use a stable message key for history reconciliation');
assert(frontendSource.includes('data-message-key'), 'Message DOM nodes should retain stable message keys');
assert(
(frontendSource.match(/indexed\.set\(keyed\.get\(key\), message\)/g) || []).length >= 2,
'History snapshot merges should update an existing stable ID in place'
);
assert(serverSource.includes('function mergeNativeHistoryWithSnapshot'), 'Server should merge native history into the snapshot prefix');
assert(serverSource.includes('historySnapshotBaseIndex'), 'Persisted snapshots should record their logical history base index');
assert(frontendSource.includes('currentOutlineHistoryState'),
'User-message locator should keep an independent full-history index state');
assert(frontendSource.includes('function requestOutlineHistoryPage'),
@@ -4679,12 +4687,82 @@ function assertHistoryRecallContract() {
});
const parsed = store.parseCodexRolloutLines([
JSON.stringify({ type: 'session_meta', payload: { id: 'history-thread', cwd: '/tmp' } }),
JSON.stringify({ type: 'event_msg', timestamp: '2026-09-11T00:00:00.000Z', payload: { type: 'task_started', turn_id: 'turn-history' } }),
JSON.stringify({ type: 'event_msg', timestamp: '2026-09-11T00:00:00.000Z', payload: { type: 'user_message', message: '旧问题' } }),
JSON.stringify({ type: 'response_item', timestamp: '2026-09-11T00:00:01.000Z', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: '旧回答' }] } }),
JSON.stringify({ type: 'response_item', timestamp: '2026-09-11T00:00:01.000Z', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: '旧回答第一段' }] } }),
JSON.stringify({ type: 'response_item', timestamp: '2026-09-11T00:00:02.000Z', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: '旧回答第二段' }] } }),
JSON.stringify({ type: 'response_item', timestamp: '2026-09-11T00:00:03.000Z', payload: { type: 'custom_tool_call', call_id: 'custom-1', name: 'ccweb_send_message', input: '{}' } }),
JSON.stringify({ type: 'response_item', timestamp: '2026-09-11T00:00:04.000Z', payload: { type: 'custom_tool_call_output', call_id: 'custom-1', output: 'done' } }),
JSON.stringify({ type: 'event_msg', timestamp: '2026-09-11T00:00:05.000Z', payload: { type: 'task_complete', turn_id: 'turn-history' } }),
JSON.stringify({ type: 'event_msg', timestamp: '2026-09-11T00:00:06.000Z', payload: { type: 'task_started', turn_id: 'turn-cross' } }),
JSON.stringify({ type: 'event_msg', timestamp: '2026-09-11T00:00:07.000Z', payload: { type: 'user_message', message: '来自「源会话」对话(ID: 00000000-0000-0000-0000-000000000001)的消息:内部输入' } }),
JSON.stringify({ type: 'response_item', timestamp: '2026-09-11T00:00:08.000Z', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: '跨对话回复' }] } }),
]);
assert(parsed.messages.length === 2, 'Codex rollout parser should recover user and assistant history');
assert(parsed.messages[0]?.content === '旧问题' && parsed.messages[1]?.content === '旧回答',
assert(parsed.messages.length === 3, 'Codex rollout parser should recover user and assistant history');
assert(parsed.messages[0]?.content === '旧问题' && parsed.messages[1]?.content === '旧回答第一段\n\n旧回答第二段',
'Recovered native history should preserve message order and content');
assert(parsed.messages[1]?.nativeTurnKey === 'native:history-thread:turn-history', 'Aggregated native assistant should retain its turn key');
assert(parsed.messages[1]?.toolCalls?.length === 1, 'Aggregated native assistant should retain custom tool calls');
assert(!parsed.messages.some((message) => message.role === 'user' && message.content.includes('内部输入')),
'Cross-conversation native input must not become a regular user bubble');
const implicitTurns = store.parseCodexRolloutLines([
JSON.stringify({ type: 'session_meta', payload: { id: 'implicit-thread' } }),
JSON.stringify({ type: 'event_msg', timestamp: '2026-09-11T00:01:00.000Z', payload: { type: 'user_message', message: '第一轮问题' } }),
JSON.stringify({ type: 'response_item', timestamp: '2026-09-11T00:01:01.000Z', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: '第一轮回答' }] } }),
JSON.stringify({ type: 'event_msg', timestamp: '2026-09-11T00:02:00.000Z', payload: { type: 'user_message', message: '第二轮问题' } }),
JSON.stringify({ type: 'response_item', timestamp: '2026-09-11T00:02:01.000Z', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: '第二轮回答' }] } }),
]);
const implicitAssistants = implicitTurns.messages.filter((message) => message.role === 'assistant');
assert(implicitAssistants.length === 2 && implicitAssistants[0].nativeTurnKey !== implicitAssistants[1].nativeTurnKey,
'Rollout parser should assign distinct stable keys to adjacent unmarked turns');
assert(
/existingElement\.dataset\.messageFingerprint[\s\S]*?existingElement\.replaceWith\(replacement\)/.test(frontendSource),
'Realtime session messages with an existing stable key should replace changed content instead of being dropped'
);
const historyMergeStart = serverSource.indexOf('function stableMessageHash');
const historyMergeEnd = serverSource.indexOf('function sessionHistoryCacheKey', historyMergeStart);
assert(historyMergeStart >= 0 && historyMergeEnd > historyMergeStart,
'Server should keep an extractable stable-ID history merge implementation');
const historyMergeApi = new Function('crypto', `${serverSource.slice(historyMergeStart, historyMergeEnd)}; return { mergeNativeHistoryWithSnapshot };`)(require('crypto'));
const snapshotTail = [
{ id: 'client-user-1', role: 'user', content: '没参考其他的页面么,WMSA51 是你这么写的么?' },
{ id: 'codex-turn-1', role: 'assistant', content: '先按项目规范核对现有页面、任务票和负责会话。' },
];
const merged = historyMergeApi.mergeNativeHistoryWithSnapshot(
{ historySnapshotBaseIndex: 1 },
snapshotTail,
[
{ id: 'native-old', role: 'assistant', content: '更早的 native 历史' },
...snapshotTail,
],
);
assert(merged.confirmed === true, 'History merge should confirm an overlapping native prefix');
assert(JSON.stringify(merged.messages.map((message) => message.id))
=== JSON.stringify(['native-old', 'client-user-1', 'codex-turn-1']),
'History merge should keep the persisted user and assistant tail adjacent');
const duplicatedNative = historyMergeApi.mergeNativeHistoryWithSnapshot(
{ historySnapshotBaseIndex: 1 },
snapshotTail,
[
{ id: 'native-old', role: 'assistant', content: '更早的 native 历史' },
snapshotTail[0],
{ ...snapshotTail[0], content: 'native 重复用户消息' },
snapshotTail[1],
],
);
assert(JSON.stringify(duplicatedNative.messages.map((message) => message.id))
=== JSON.stringify(['native-old', 'client-user-1', 'codex-turn-1']),
'History merge should remove duplicate native IDs when the snapshot is authoritative');
const unconfirmed = historyMergeApi.mergeNativeHistoryWithSnapshot(
{ historySnapshotBaseIndex: 0 },
snapshotTail,
[{ id: 'native-unrelated', role: 'assistant', content: '无法确认的 native 消息' }],
);
assert(unconfirmed.confirmed === false && unconfirmed.messages.length === snapshotTail.length,
'Unconfirmed native history must fall back to the cc-web snapshot');
}
function assertSessionSwitchRaceContract() {