修复历史边界与流式消息去重并更新发布包

This commit is contained in:
shiyue
2026-09-21 00:38:51 +08:00
parent db04e088bc
commit f8f5ef3a0b
14 changed files with 1733 additions and 423 deletions

View File

@@ -2818,6 +2818,7 @@ function assertSessionProjectSnapshotContract() {
return new Date(b.updated || 0) - new Date(a.updated || 0);
}
${normalizeGiteaSourceSource}
${['stableMessageHash', 'getStableMessageKey', 'mergeIndexedSessionMessages'].map((name) => extractFunctionSource(frontendSource, name)).join('\n')}
${mergeSource}
return {
mergeSessionListSnapshot,
@@ -3799,6 +3800,9 @@ function assertSessionRenderEpochRaceContract() {
function scrollToBottom() {}
function updateScrollbar() {}
function buildMsgElement(message) { return { id: message.id }; }
function getStableMessageKey(message) { return message.id; }
function findRenderedSessionMessageByKey(key) { return messagesDiv.nodes.find((node) => node.id === key); }
function prependHistoryMessages(messages) { messagesDiv.insertBefore({ __fragment: true, nodes: messages.map(buildMsgElement) }); }
${beginSessionSwitchSource}
${renderMessagesSource}
return {
@@ -3929,6 +3933,7 @@ function assertSessionRequestIdRaceContract() {
function bufferSessionSnapshot() {}
function scheduleAdvancedSearchJump() {}
function finalizeLoadedSession(sessionId, requestId) { finalized.push({ sessionId, requestId }); }
${['stableMessageHash', 'getStableMessageKey', 'mergeIndexedSessionMessages'].map((name) => extractFunctionSource(frontendSource, name)).join('\n')}
${historyMergeSource}
function handleHistoryMessage(msg) {
switch (msg.type) {
@@ -4174,6 +4179,7 @@ function assertRecoverCurrentHistoryMergeContract() {
function scheduleAdvancedSearchJump() {}
function cacheSessionSnapshot(snapshot) { cached.push(JSON.parse(JSON.stringify(snapshot))); }
function finishSessionSwitch(sessionId, requestId) { finished.push({ sessionId, requestId }); }
${['stableMessageHash', 'getStableMessageKey', 'mergeIndexedSessionMessages'].map((name) => extractFunctionSource(frontendSource, name)).join('\n')}
${historyMergeSource}
${finalizeLoadedSessionSource}
function handleHistoryMessage(msg) {
@@ -4273,6 +4279,7 @@ function assertPartialSessionSnapshotCacheContract() {
${bufferSource}
${getBufferSource}
${cacheSource}
${['stableMessageHash', 'getStableMessageKey', 'mergeIndexedSessionMessages'].map((name) => extractFunctionSource(frontendSource, name)).join('\n')}
${mergeSource}
${dispositionSource}
return {
@@ -4562,6 +4569,11 @@ function assertServerSessionHistoryRequestIdContract() {
}
function assertHistoryRecallContract() {
for (const script of ['session-history-unit.js', 'codex-rollouts-unit.js', 'history-frontend-unit.js']) {
const result = spawnSync(process.execPath, [path.join(REPO_DIR, 'scripts', script)], { encoding: 'utf8', timeout: 60000 });
assert(result.status === 0, `${script}: ${result.stderr || result.stdout || result.error}`);
}
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
const indexSource = fs.readFileSync(PUBLIC_INDEX_PATH, 'utf8');
@@ -4576,16 +4588,11 @@ function assertHistoryRecallContract() {
assert(frontendSource.includes('function requestOlderHistory'), 'Frontend should request older history on demand');
assert(frontendSource.includes("type: 'load_history_page'"), 'Frontend should use the paged history WebSocket request');
assert(frontendSource.includes('mergeHistoryChunkIntoCachedSnapshot'), 'Frontend should merge history pages into the session cache');
assert(
/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(!extractFunctionSource(frontendSource, 'reconcileRenderedSessionMessages').includes('if (existing.has(messageIndex)) return'),
'Different IDs must not be dropped just because their indexes collide');
assert(frontendSource.includes('function mergeIndexedSessionMessages'), 'History pages should share ID-based merging');
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'),
@@ -4606,8 +4613,8 @@ function assertHistoryRecallContract() {
/messagesDiv\.innerHTML = '';\s*if \(historyLoadMore\) messagesDiv\.appendChild\(historyLoadMore\)/.test(frontendSource),
'Message rerenders should preserve the in-content history control');
assert(
/messagesDiv\.insertBefore\(frag, historyLoadMore\?\.nextSibling \|\| null\)/.test(frontendSource),
'Older message batches should be inserted below the in-content history control');
extractFunctionSource(frontendSource, 'prependHistoryMessages').includes('reconcileRenderedSessionMessages'),
'Older pages should use stable-ID reconciliation while preserving the in-content history control');
const historyHandlerStart = frontendSource.indexOf("case 'session_history_chunk':");
const historyHandlerEnd = frontendSource.indexOf("case 'session_message':", historyHandlerStart);
const historyHandlerSource = historyHandlerStart >= 0 && historyHandlerEnd > historyHandlerStart
@@ -4717,10 +4724,7 @@ function assertHistoryRecallContract() {
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'
);
assert(frontendSource.includes('getMessageRenderFingerprint'), 'History rendering should fingerprint tool results and source metadata too');
const historyMergeStart = serverSource.indexOf('function stableMessageHash');
const historyMergeEnd = serverSource.indexOf('function sessionHistoryCacheKey', historyMergeStart);
@@ -4832,7 +4836,7 @@ function assertCodexAppGoalLifecycleContract() {
assert(
doneBlock.includes('snapshot.isRunning = msg.goalActive === true')
&& doneBlock.includes('finishGenerating(msg.sessionId, { keepRunning: msg.goalActive === true })'),
&& /finishGenerating\(msg.sessionId,\s*\{\s*keepRunning: msg.goalActive === true/.test(doneBlock),
'Frontend done handling should keep current and background Goal sessions running between turns'
);
assert(
@@ -4859,7 +4863,7 @@ function assertCodexAppGoalLifecycleContract() {
);
assert(
completeBlock.includes('const goalActive = isCodexAppGoalActive(sessionId)')
&& completeBlock.includes("{ type: 'done', sessionId, costUsd: null, goalActive }")
&& completeBlock.includes("type: 'done', sessionId, costUsd: null, goalActive")
&& completeBlock.includes('if (goalActive)')
&& completeBlock.includes('broadcastSessionList()'),
'Server turn completion should report active Goal state without sending final background completion'