This commit is contained in:
Timothy Jaeryang Baek
2026-05-19 20:12:00 +04:00
parent 359590ca9d
commit d310a0777c
2 changed files with 79 additions and 23 deletions
+4 -23
View File
@@ -58,6 +58,7 @@
copyToClipboard,
getMessageContentParts,
createMessagesList,
sanitizeHistory,
getPromptVariables,
processDetails,
removeAllDetails,
@@ -1379,29 +1380,9 @@
? chatContent.history
: convertMessagesToHistory(chatContent.messages);
// Sanitize history: repair orphaned references from failed regenerations (#24424)
for (const message of Object.values(history.messages)) {
if (message.childrenIds) {
message.childrenIds = message.childrenIds.filter(
(childId) => history.messages[childId]
);
}
}
if (history.currentId && !history.messages[history.currentId]) {
const messageIds = Object.keys(history.messages);
let lastMessageId = null;
for (const messageId of messageIds) {
const message = history.messages[messageId];
if (
(message.childrenIds ?? []).length === 0 &&
(!lastMessageId ||
(message.timestamp ?? 0) > (history.messages[lastMessageId].timestamp ?? 0))
) {
lastMessageId = messageId;
}
}
history.currentId = lastMessageId ?? messageIds[0] ?? null;
}
// Sanitize history: repair orphaned references and structurally-malformed
// nodes from failed regenerations (#24424, #24157, #20474)
sanitizeHistory(history);
chatTitle.set(chatContent.title);
+75
View File
@@ -225,6 +225,81 @@ export const convertMessagesToHistory = (messages) => {
return history;
};
// Repair structurally-malformed history nodes from failed regenerations.
// A lost assistant placeholder may exist under its map key with only
// completion fields (content/done/error), missing id/role/parentId.
// Reconstruct graph fields so already-corrupted chats recover on open.
export const sanitizeHistory = (history) => {
if (!history?.messages || typeof history.messages !== 'object') return;
// Purge entries that aren't usable objects
for (const [id, message] of Object.entries(history.messages)) {
if (!message || typeof message !== 'object') {
delete history.messages[id];
}
}
// Ensure every surviving node has its canonical id and a childrenIds array
for (const [id, message] of Object.entries(history.messages)) {
if (message.id !== id) message.id = id;
if (!Array.isArray(message.childrenIds)) message.childrenIds = [];
}
// Build reverse lookup: parent, indexed by child id
const parentByChildId = {};
for (const [id, message] of Object.entries(history.messages)) {
for (const childId of message.childrenIds) {
parentByChildId[childId] = id;
}
}
// Reconstruct missing parentId and role
for (const [id, message] of Object.entries(history.messages)) {
// Well-formed: has role and explicit parentId (null is valid for root)
if (message.role && message.parentId !== undefined) continue;
if (message.parentId === undefined) {
message.parentId = parentByChildId[id] ?? null;
}
if (!message.role) {
const parent = message.parentId ? history.messages[message.parentId] : null;
message.role =
parent?.role === 'user'
? 'assistant'
: parent?.role === 'assistant'
? 'user'
: message.model || message.usage || message.done !== undefined
? 'assistant'
: 'user';
}
}
// Prune childrenIds referencing deleted/missing nodes
for (const message of Object.values(history.messages)) {
message.childrenIds = message.childrenIds.filter(
(childId) => history.messages[childId]
);
}
// Recover currentId if it points to a missing or incomplete node
const currentMessage = history.messages?.[history.currentId];
if (!currentMessage?.id || !currentMessage?.role) {
let latestLeafId = null;
let latestTimestamp = -1;
for (const [id, message] of Object.entries(history.messages)) {
if (
message.childrenIds.length === 0 &&
(message.timestamp ?? 0) > latestTimestamp
) {
latestLeafId = id;
latestTimestamp = message.timestamp ?? 0;
}
}
history.currentId = latestLeafId ?? Object.keys(history.messages)[0] ?? null;
}
};
export const getGravatarURL = (email) => {
// Trim leading and trailing whitespace from
// an email address and force all characters