修复 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

@@ -1795,6 +1795,52 @@
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
}
function stableMessageHash(value) {
let hash = 2166136261;
for (const char of String(value || '')) {
hash ^= char.charCodeAt(0);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0).toString(16);
}
function getStableMessageKey(message) {
if (!message || typeof message !== 'object') return '';
const explicit = message.id
|| message.messageId
|| message.clientMessageId
|| message.replyToRequestId
|| message.crossConversation?.replyToRequestId
|| message.codexAppTurnKey
|| message.nativeTurnKey;
if (explicit) return String(explicit);
const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content || '');
return `message:${stableMessageHash(`${message.role || ''}\u001f${message.timestamp || ''}\u001f${content}`)}`;
}
function findRenderedSessionMessageByKey(key) {
const normalizedKey = String(key || '').trim();
if (!normalizedKey) return null;
return Array.from(messagesDiv.querySelectorAll('[data-session-message="true"][data-message-key]'))
.find((element) => element.dataset.messageKey === normalizedKey) || null;
}
function dedupeSessionMessages(messages) {
if (!Array.isArray(messages)) return [];
const result = [];
const positions = new Map();
messages.forEach((message) => {
const key = getStableMessageKey(message);
if (key && positions.has(key)) {
result[positions.get(key)] = message;
} else {
if (key) positions.set(key, result.length);
result.push(message);
}
});
return result;
}
function rememberPendingSlashDraft(requestId, text, sessionId, agent) {
const id = String(requestId || '').trim();
if (!id) return;
@@ -2442,10 +2488,16 @@
getMessageActionRow(bubble)?.appendChild(button);
}
function markSessionMessageElement(messageEl, messageIndex) {
function markSessionMessageElement(messageEl, messageIndex, message = null) {
if (!messageEl || !Number.isFinite(messageIndex) || messageIndex < 0) return;
messageEl.dataset.sessionMessage = 'true';
messageEl.dataset.messageIndex = String(messageIndex);
if (message) {
const key = getStableMessageKey(message);
if (key) messageEl.dataset.messageKey = key;
const rawContent = typeof message.content === 'string' ? message.content : JSON.stringify(message.content || '');
messageEl.dataset.messageFingerprint = stableMessageHash(`${message.role || ''}\u001f${rawContent}`);
}
if (messageEl.classList.contains('assistant')) {
syncAssistantBranchButton(messageEl, messageIndex);
}
@@ -7000,9 +7052,27 @@
? Number(snapshot.historyBaseIndex)
: Math.max(0, Number(snapshot.historyTotal || existingMessages.length) - existingMessages.length);
const incomingBase = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
const stableKey = (message) => typeof getStableMessageKey === 'function'
? getStableMessageKey(message)
: String(message?.id || message?.messageId || '').trim();
const indexed = new Map();
existingMessages.forEach((message, index) => indexed.set(existingBase + index, message));
messages.forEach((message, index) => indexed.set(incomingBase + index, message));
const keyed = new Map();
existingMessages.forEach((message, index) => {
const position = existingBase + index;
indexed.set(position, message);
const key = stableKey(message);
if (key) keyed.set(key, position);
});
messages.forEach((message, index) => {
const key = stableKey(message);
if (key && keyed.has(key)) {
indexed.set(keyed.get(key), message);
return;
}
const position = incomingBase + index;
indexed.set(position, message);
if (key) keyed.set(key, position);
});
const indexes = Array.from(indexed.keys()).sort((a, b) => a - b);
snapshot.messages = indexes.map((index) => indexed.get(index));
snapshot.historyBaseIndex = indexes.length > 0 ? indexes[0] : incomingBase;
@@ -7029,9 +7099,27 @@
? Number(snapshot.historyBaseIndex)
: Math.max(0, Number(snapshot.historyTotal || existingMessages.length) - existingMessages.length);
const incomingBase = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
const stableKey = (message) => typeof getStableMessageKey === 'function'
? getStableMessageKey(message)
: String(message?.id || message?.messageId || '').trim();
const indexed = new Map();
existingMessages.forEach((message, index) => indexed.set(existingBase + index, message));
messages.forEach((message, index) => indexed.set(incomingBase + index, message));
const keyed = new Map();
existingMessages.forEach((message, index) => {
const position = existingBase + index;
indexed.set(position, message);
const key = stableKey(message);
if (key) keyed.set(key, position);
});
messages.forEach((message, index) => {
const key = stableKey(message);
if (key && keyed.has(key)) {
indexed.set(keyed.get(key), message);
return;
}
const position = incomingBase + index;
indexed.set(position, message);
if (key) keyed.set(key, position);
});
const indexes = Array.from(indexed.keys()).sort((a, b) => a - b);
snapshot.messages = indexes.map((index) => indexed.get(index));
snapshot.historyBaseIndex = indexes.length > 0 ? indexes[0] : incomingBase;
@@ -8256,9 +8344,14 @@
if (msg.sessionId && msg.message) {
const isCrossConversationReply = !!msg.message.crossConversation?.replyToRequestId;
const isProvisionalCodexAppSteer = !!msg.message.codexAppSteerStatus;
const messageKey = getStableMessageKey(msg.message);
updateCachedSession(msg.sessionId, (snapshot) => {
snapshot.messages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
if (!isProvisionalCodexAppSteer) snapshot.messages.push(deepClone(msg.message));
if (!isProvisionalCodexAppSteer) {
const existingIndex = snapshot.messages.findIndex((item) => getStableMessageKey(item) === messageKey);
if (existingIndex >= 0) snapshot.messages[existingIndex] = deepClone(msg.message);
else snapshot.messages.push(deepClone(msg.message));
}
snapshot.updated = msg.message.timestamp || new Date().toISOString();
if (isCrossConversationReply) {
snapshot.readyReplyCount = Math.max(0, Number(snapshot.readyReplyCount || 0) - 1);
@@ -8282,6 +8375,25 @@
}
if (msg.sessionId === currentSessionId && msg.message) {
const isProvisionalCodexAppSteer = !!msg.message.codexAppSteerStatus;
const existingElement = !isProvisionalCodexAppSteer
? findRenderedSessionMessageByKey(getStableMessageKey(msg.message))
: null;
if (existingElement) {
const rawContent = typeof msg.message.content === 'string'
? msg.message.content
: JSON.stringify(msg.message.content || '');
const fingerprint = stableMessageHash(`${msg.message.role || ''}\u001f${rawContent}`);
if (existingElement.dataset.messageFingerprint !== fingerprint) {
const messageIndex = Number(existingElement.dataset.messageIndex);
const replacement = buildMsgElement(
msg.message,
Number.isFinite(messageIndex) ? messageIndex : null,
);
existingElement.replaceWith(replacement);
}
renderSessionList();
break;
}
const messageIndex = isProvisionalCodexAppSteer ? null : currentSessionMessageCount;
if (!isProvisionalCodexAppSteer) currentSessionMessageCount += 1;
collectClosedCollabAgentIds([msg.message]).forEach((id) => closedCollabAgentIds.add(id));
@@ -8542,7 +8654,7 @@
const historyTotal = Number.isFinite(Number(msg.historyTotal))
? Math.max(0, Number(msg.historyTotal))
: msg.messages.length;
if (renderedMessages < historyTotal) {
if (renderedMessages < historyTotal || msg.messages.some((message) => !findRenderedSessionMessageByKey(getStableMessageKey(message)))) {
reconcileRenderedSessionMessages(msg.messages, {
preserveScroll: false,
baseIndex: Number.isFinite(Number(msg.historyBaseIndex))
@@ -9178,13 +9290,19 @@
|| (hasGiteaPrompt ? { source: 'gitea', label: 'Gitea', repository: '', resourceKind: 'issue', number: null, title: '' } : null);
const isGiteaMessage = role === 'user' && !!giteaSource;
const visibleContent = isGiteaMessage ? getGiteaVisibleMessageContent(content) : content;
const resolvedMessageId = meta?.messageId || meta?.id || createLocalId('user');
const resolvedMessageId = meta?.messageId || meta?.id || getStableMessageKey(meta) || createLocalId('message');
div.className = `msg ${role}${role === 'assistant' ? ' agent-' + currentAgent : ''}${isCrossConversation ? ' cross-conversation' : ''}${isCrossConversationReply ? ' cross-conversation-reply' : ''}${isGoalMessage ? ' goal-message' : ''}${isGiteaMessage ? ' gitea-message' : ''}`;
if (role === 'user') {
div.id = `hapi-message-${resolvedMessageId}`;
div.dataset.messageId = resolvedMessageId;
if (isGoalMessage) div.dataset.goalMode = 'true';
}
if (role !== 'system') {
div.dataset.messageId = resolvedMessageId;
div.dataset.messageKey = getStableMessageKey(meta) || resolvedMessageId;
const rawContent = typeof content === 'string' ? content : JSON.stringify(content || '');
div.dataset.messageFingerprint = stableMessageHash(`${role}\u001f${rawContent}`);
}
if (role === 'system') {
const tone = String(meta.tone || 'neutral').trim() || 'neutral';
@@ -10788,12 +10906,13 @@
}
}
if (Number.isFinite(messageIndex)) {
markSessionMessageElement(el, messageIndex);
markSessionMessageElement(el, messageIndex, m);
}
return el;
}
function renderMessages(messages, options = {}) {
messages = typeof dedupeSessionMessages === 'function' ? dedupeSessionMessages(messages) : messages;
renderEpoch++;
const epoch = renderEpoch;
const baseIndex = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
@@ -10869,11 +10988,16 @@
const existingIndexes = new Set(Array.from(
messagesDiv.querySelectorAll('[data-session-message="true"][data-message-index]'),
).map((element) => Number(element.dataset.messageIndex)));
const existingKeys = new Set(Array.from(
messagesDiv.querySelectorAll('[data-session-message="true"][data-message-key]'),
).map((element) => String(element.dataset.messageKey || '').trim()).filter(Boolean));
const uniqueMessages = [];
messages.forEach((message, index) => {
const messageIndex = baseIndex + index;
if (existingIndexes.has(messageIndex)) return;
const key = getStableMessageKey(message);
if (existingIndexes.has(messageIndex) || (key && existingKeys.has(key))) return;
existingIndexes.add(messageIndex);
if (key) existingKeys.add(key);
uniqueMessages.push({ message, index });
});
if (uniqueMessages.length === 0) return;
@@ -11586,9 +11710,12 @@
if (!Array.isArray(messages) || messages.length === 0) return;
const baseIndex = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
const existing = new Map();
const existingByKey = new Map();
messagesDiv.querySelectorAll('[data-session-message="true"][data-message-index]').forEach((element) => {
const index = Number(element.dataset.messageIndex);
if (Number.isFinite(index)) existing.set(index, element);
const key = String(element.dataset.messageKey || '').trim();
if (key) existingByKey.set(key, element);
});
const beforeHeight = messagesDiv.scrollHeight;
const beforeScrollTop = messagesDiv.scrollTop;
@@ -11596,6 +11723,19 @@
let inserted = 0;
messages.forEach((message, offset) => {
const messageIndex = baseIndex + offset;
const messageKey = getStableMessageKey(message);
const keyed = existingByKey.get(messageKey);
if (keyed) {
const rawContent = typeof message.content === 'string' ? message.content : JSON.stringify(message.content || '');
const fingerprint = stableMessageHash(`${message.role || ''}\u001f${rawContent}`);
if (keyed.dataset.messageFingerprint !== fingerprint) {
const replacement = buildMsgElement(message, messageIndex);
keyed.replaceWith(replacement);
existingByKey.set(messageKey, replacement);
existing.set(messageIndex, replacement);
}
return;
}
if (existing.has(messageIndex)) return;
const element = buildMsgElement(message, messageIndex);
const next = Array.from(existing.entries())
@@ -11603,6 +11743,7 @@
.sort(([left], [right]) => left - right)[0]?.[1];
messagesDiv.insertBefore(element, next || stream || null);
existing.set(messageIndex, element);
if (messageKey) existingByKey.set(messageKey, element);
inserted += 1;
});
if (inserted === 0) return;
@@ -12461,13 +12602,21 @@
if (currentSessionId) {
const messageIndex = currentSessionMessageCount;
currentSessionMessageCount += 1;
markSessionMessageElement(element, messageIndex);
markSessionMessageElement(element, messageIndex, { id: messageId, role: 'user', content: text, timestamp });
}
registerUserMessage(messageId, element, text, timestamp);
updateUserOutlinePanel();
scrollToBottom();
send({ type: 'message', text, attachments, sessionId: currentSessionId, mode: currentMode, agent: currentAgent });
send({
type: 'message',
text,
attachments,
sessionId: currentSessionId,
mode: currentMode,
agent: currentAgent,
clientMessageId: messageId,
});
startGenerating();
}