fix: sync local message ID to backend via acpSendMessage

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 <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-04-17 11:01:10 -10:00
parent c7e96edc40
commit becf37f2a9
4 changed files with 34 additions and 25 deletions
@@ -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,
},
}),
);
});
});
@@ -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 () => {
@@ -244,6 +244,7 @@ export function useChat(
(img) => [img.base64, img.mimeType] as [string, string],
),
truncateBeforeMessageId,
messageId: userMessage.id,
});
store.setChatState(sessionId, "idle");
+5 -1
View File
@@ -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);