♻️ fix: Correct Message ID Assignment Logic (#8439)

* fix: Add `isRegenerate` flag to chat payload to avoid saving temporary response IDs

* fix: Remove unused `isResubmission` flag

* ci: Add tests for responseMessageId regeneration logic in BaseClient
This commit is contained in:
Danny Avila 2025-07-14 00:57:20 -04:00 committed by GitHub
parent 170cc340d8
commit e370a87ebe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 61 additions and 15 deletions

View file

@ -197,6 +197,10 @@ class BaseClient {
this.currentMessages[this.currentMessages.length - 1].messageId = head; this.currentMessages[this.currentMessages.length - 1].messageId = head;
} }
if (opts.isRegenerate && responseMessageId.endsWith('_')) {
responseMessageId = crypto.randomUUID();
}
this.responseMessageId = responseMessageId; this.responseMessageId = responseMessageId;
return { return {

View file

@ -422,6 +422,46 @@ describe('BaseClient', () => {
expect(response).toEqual(expectedResult); expect(response).toEqual(expectedResult);
}); });
test('should replace responseMessageId with new UUID when isRegenerate is true and messageId ends with underscore', async () => {
const mockCrypto = require('crypto');
const newUUID = 'new-uuid-1234';
jest.spyOn(mockCrypto, 'randomUUID').mockReturnValue(newUUID);
const opts = {
isRegenerate: true,
responseMessageId: 'existing-message-id_',
};
await TestClient.setMessageOptions(opts);
expect(TestClient.responseMessageId).toBe(newUUID);
expect(TestClient.responseMessageId).not.toBe('existing-message-id_');
mockCrypto.randomUUID.mockRestore();
});
test('should not replace responseMessageId when isRegenerate is false', async () => {
const opts = {
isRegenerate: false,
responseMessageId: 'existing-message-id_',
};
await TestClient.setMessageOptions(opts);
expect(TestClient.responseMessageId).toBe('existing-message-id_');
});
test('should not replace responseMessageId when it does not end with underscore', async () => {
const opts = {
isRegenerate: true,
responseMessageId: 'existing-message-id',
};
await TestClient.setMessageOptions(opts);
expect(TestClient.responseMessageId).toBe('existing-message-id');
});
test('sendMessage should work with provided conversationId and parentMessageId', async () => { test('sendMessage should work with provided conversationId and parentMessageId', async () => {
const userMessage = 'Second message in the conversation'; const userMessage = 'Second message in the conversation';
const opts = { const opts = {

View file

@ -12,6 +12,7 @@ const { saveMessage } = require('~/models');
const AgentController = async (req, res, next, initializeClient, addTitle) => { const AgentController = async (req, res, next, initializeClient, addTitle) => {
let { let {
text, text,
isRegenerate,
endpointOption, endpointOption,
conversationId, conversationId,
isContinued = false, isContinued = false,
@ -167,6 +168,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
onStart, onStart,
getReqData, getReqData,
isContinued, isContinued,
isRegenerate,
editedContent, editedContent,
conversationId, conversationId,
parentMessageId, parentMessageId,

View file

@ -345,9 +345,7 @@ export type TOptions = {
isContinued?: boolean; isContinued?: boolean;
isEdited?: boolean; isEdited?: boolean;
overrideMessages?: t.TMessage[]; overrideMessages?: t.TMessage[];
/** This value is only true when the user submits a message with "Save & Submit" for a user-created message */ /** Currently only utilized when resubmitting user-created message, uses that message's currently attached files */
isResubmission?: boolean;
/** Currently only utilized when `isResubmission === true`, uses that message's currently attached files */
overrideFiles?: t.TMessage['files']; overrideFiles?: t.TMessage['files'];
}; };

View file

@ -60,7 +60,6 @@ const EditMessage = ({
conversationId, conversationId,
}, },
{ {
isResubmission: true,
overrideFiles: message.files, overrideFiles: message.files,
}, },
); );

View file

@ -83,7 +83,6 @@ export default function useChatFunctions({
{ {
editedContent = null, editedContent = null,
editedMessageId = null, editedMessageId = null,
isResubmission = false,
isRegenerate = false, isRegenerate = false,
isContinued = false, isContinued = false,
isEdited = false, isEdited = false,
@ -230,7 +229,10 @@ export default function useChatFunctions({
} }
const responseMessageId = const responseMessageId =
editedMessageId ?? (latestMessage?.messageId ? latestMessage?.messageId + '_' : null) ?? null; editedMessageId ??
(latestMessage?.messageId && isRegenerate ? latestMessage?.messageId + '_' : null) ??
null;
const initialResponse: TMessage = { const initialResponse: TMessage = {
sender: responseSender, sender: responseSender,
text: '', text: '',
@ -307,7 +309,6 @@ export default function useChatFunctions({
isEdited: isEditOrContinue, isEdited: isEditOrContinue,
isContinued, isContinued,
isRegenerate, isRegenerate,
isResubmission,
initialResponse, initialResponse,
isTemporary, isTemporary,
ephemeralAgent, ephemeralAgent,

View file

@ -4,14 +4,15 @@ import * as s from './schemas';
export default function createPayload(submission: t.TSubmission) { export default function createPayload(submission: t.TSubmission) {
const { const {
conversation,
userMessage,
endpointOption,
isEdited, isEdited,
userMessage,
isContinued, isContinued,
isTemporary, isTemporary,
ephemeralAgent, isRegenerate,
conversation,
editedContent, editedContent,
ephemeralAgent,
endpointOption,
} = submission; } = submission;
const { conversationId } = s.tConvoUpdateSchema.parse(conversation); const { conversationId } = s.tConvoUpdateSchema.parse(conversation);
const { endpoint: _e, endpointType } = endpointOption as { const { endpoint: _e, endpointType } = endpointOption as {
@ -31,11 +32,12 @@ export default function createPayload(submission: t.TSubmission) {
...userMessage, ...userMessage,
...endpointOption, ...endpointOption,
endpoint, endpoint,
ephemeralAgent: s.isAssistantsEndpoint(endpoint) ? undefined : ephemeralAgent,
isContinued: !!(isEdited && isContinued),
conversationId,
isTemporary, isTemporary,
isRegenerate,
editedContent, editedContent,
conversationId,
isContinued: !!(isEdited && isContinued),
ephemeralAgent: s.isAssistantsEndpoint(endpoint) ? undefined : ephemeralAgent,
}; };
return { server, payload }; return { server, payload };

View file

@ -105,6 +105,7 @@ export type TEphemeralAgent = {
export type TPayload = Partial<TMessage> & export type TPayload = Partial<TMessage> &
Partial<TEndpointOption> & { Partial<TEndpointOption> & {
isContinued: boolean; isContinued: boolean;
isRegenerate?: boolean;
conversationId: string | null; conversationId: string | null;
messages?: TMessages; messages?: TMessages;
isTemporary: boolean; isTemporary: boolean;
@ -125,7 +126,6 @@ export type TSubmission = {
isTemporary: boolean; isTemporary: boolean;
messages: TMessage[]; messages: TMessage[];
isRegenerate?: boolean; isRegenerate?: boolean;
isResubmission?: boolean;
initialResponse?: TMessage; initialResponse?: TMessage;
conversation: Partial<TConversation>; conversation: Partial<TConversation>;
endpointOption: TEndpointOption; endpointOption: TEndpointOption;