From becf37f2a97d62d4fd58f1530bcfc4084b7791d3 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Fri, 17 Apr 2026 11:01:10 -1000 Subject: [PATCH] fix: sync local message ID to backend via acpSendMessage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass userMessage.id from the local Zustand store through to acpSendMessage so the backend stores the same UUID. Without this, the frontend and backend have different IDs for the same user message, causing truncate_before_message_id lookups to find nothing — edit/retry truncation silently fails and messages zombie back on session reload. Signed-off-by: Taylor Ho --- .../__tests__/useChat.attachments.test.ts | 16 ++++----- .../chat/hooks/__tests__/useChat.test.ts | 36 ++++++++++--------- ui/goose2/src/features/chat/hooks/useChat.ts | 1 + ui/goose2/src/shared/api/acp.ts | 6 +++- 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/ui/goose2/src/features/chat/hooks/__tests__/useChat.attachments.test.ts b/ui/goose2/src/features/chat/hooks/__tests__/useChat.attachments.test.ts index da14807a07..d22e640e9d 100644 --- a/ui/goose2/src/features/chat/hooks/__tests__/useChat.attachments.test.ts +++ b/ui/goose2/src/features/chat/hooks/__tests__/useChat.attachments.test.ts @@ -95,12 +95,12 @@ describe("useChat attachments", () => { expect(mockAcpSendMessage).toHaveBeenCalledWith( "session-1", "Attached items:\n- [file] /tmp/report.pdf\n- [directory] /tmp/screenshots\nPlease review these", - { + expect.objectContaining({ systemPrompt: undefined, personaId: undefined, personaName: undefined, images: undefined, - }, + }), ); }); @@ -146,12 +146,12 @@ describe("useChat attachments", () => { expect(mockAcpSendMessage).toHaveBeenCalledWith( "session-1", "Attached items:\n- [image] diagram.png (image attached)\n ", - { + expect.objectContaining({ systemPrompt: undefined, personaId: undefined, personaName: undefined, images: [["abc123", "image/png"]], - }, + }), ); }); @@ -193,12 +193,12 @@ describe("useChat attachments", () => { expect(mockAcpSendMessage).toHaveBeenCalledWith( "session-1", "Attached items:\n- [file] /tmp/mobile-confirmation.html\n- [directory] /tmp/neighborhood block\n- [image] Screenshot 2026-04-09 at 1.25.32 PM.png (image attached)\ncan you see the attachments i attached?", - { + expect.objectContaining({ systemPrompt: undefined, personaId: undefined, personaName: undefined, images: [["abc123", "image/png"]], - }, + }), ); }); @@ -233,12 +233,12 @@ describe("useChat attachments", () => { expect(mockAcpSendMessage).toHaveBeenCalledWith( "session-1", "Attached items:\n- [file] report.pdf\nPlease review this", - { + expect.objectContaining({ systemPrompt: undefined, personaId: undefined, personaName: undefined, images: undefined, - }, + }), ); }); }); diff --git a/ui/goose2/src/features/chat/hooks/__tests__/useChat.test.ts b/ui/goose2/src/features/chat/hooks/__tests__/useChat.test.ts index bbe9009125..d46cdbef7b 100644 --- a/ui/goose2/src/features/chat/hooks/__tests__/useChat.test.ts +++ b/ui/goose2/src/features/chat/hooks/__tests__/useChat.test.ts @@ -127,12 +127,14 @@ describe("useChat", () => { result.current.stopGeneration(); }); - expect(mockAcpSendMessage).toHaveBeenCalledWith("session-1", "Hello", { - systemPrompt: undefined, - personaId: "persona-b", - personaName: "Persona B", - images: undefined, - }); + expect(mockAcpSendMessage).toHaveBeenCalledWith("session-1", "Hello", + expect.objectContaining({ + systemPrompt: undefined, + personaId: "persona-b", + personaName: "Persona B", + images: undefined, + }), + ); expect(mockAcpCancelSession).toHaveBeenCalledWith("session-1", "persona-b"); deferred.resolve(); @@ -313,23 +315,23 @@ describe("useChat", () => { 1, "session-1", "First", - { + expect.objectContaining({ systemPrompt: undefined, personaId: undefined, personaName: undefined, images: undefined, - }, + }), ); expect(mockAcpSendMessage).toHaveBeenNthCalledWith( 2, "session-2", "Second", - { + expect.objectContaining({ systemPrompt: undefined, personaId: undefined, personaName: undefined, images: undefined, - }, + }), ); deferred.resolve(); @@ -366,12 +368,14 @@ describe("useChat", () => { personaId: undefined, }); expect(mockAcpSetModel).toHaveBeenCalledWith("session-1", "gpt-4.1"); - expect(mockAcpSendMessage).toHaveBeenCalledWith("session-1", "Hello", { - systemPrompt: undefined, - personaId: undefined, - personaName: undefined, - images: undefined, - }); + expect(mockAcpSendMessage).toHaveBeenCalledWith("session-1", "Hello", + expect.objectContaining({ + systemPrompt: undefined, + personaId: undefined, + personaName: undefined, + images: undefined, + }), + ); }); it("appends an error message and removes the empty assistant placeholder when send fails", async () => { diff --git a/ui/goose2/src/features/chat/hooks/useChat.ts b/ui/goose2/src/features/chat/hooks/useChat.ts index 7db196b364..86736a00dd 100644 --- a/ui/goose2/src/features/chat/hooks/useChat.ts +++ b/ui/goose2/src/features/chat/hooks/useChat.ts @@ -244,6 +244,7 @@ export function useChat( (img) => [img.base64, img.mimeType] as [string, string], ), truncateBeforeMessageId, + messageId: userMessage.id, }); store.setChatState(sessionId, "idle"); diff --git a/ui/goose2/src/shared/api/acp.ts b/ui/goose2/src/shared/api/acp.ts index c2f23ee4e4..1d94f9089b 100644 --- a/ui/goose2/src/shared/api/acp.ts +++ b/ui/goose2/src/shared/api/acp.ts @@ -22,6 +22,10 @@ export interface AcpSendMessageOptions { * from both the display log and LLM context before appending the new prompt. * Used by edit and retry to rewind the conversation. */ truncateBeforeMessageId?: string; + /** Pre-generated message ID to use for this user message. When provided the + * same ID is registered with the notification handler and sent to the + * backend so the frontend store and backend thread_messages stay in sync. */ + messageId?: string; } export interface AcpPrepareSessionOptions { @@ -63,7 +67,7 @@ export async function acpSendMessage( ? { truncate_before_message_id: truncateBeforeMessageId } : undefined; - const messageId = crypto.randomUUID(); + const messageId = options.messageId ?? crypto.randomUUID(); setActiveMessageId(gooseSessionId, messageId); await directAcp.prompt(gooseSessionId, content, messageId, meta);