From bf32d9ec46e6b1841c14f1563930c57ba31e59d7 Mon Sep 17 00:00:00 2001 From: mameikagou Date: Fri, 9 Oct 2026 01:13:45 +0800 Subject: [PATCH] fix(chat): preserve follow-ups while the active head loads --- packages/core/chat/pending.test.ts | 63 ++++++++++++++++++++++++++++++ packages/core/chat/pending.ts | 7 +++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/packages/core/chat/pending.test.ts b/packages/core/chat/pending.test.ts index 574b181a290..d80434342ab 100644 --- a/packages/core/chat/pending.test.ts +++ b/packages/core/chat/pending.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment node import { describe, expect, it } from "vitest"; import { enqueuePendingChatTask, @@ -46,6 +47,68 @@ describe("pending chat queue", () => { }); }); + it.each(["FIFO", "reversed"])( + "preserves multiple follow-ups before the head loads (%s responses)", + (order) => { + const first = { + ...task("first", "2026-01-01T00:00:01Z"), + message_id: "message-first", + }; + const second = { + ...task("second", "2026-01-01T00:00:02Z"), + message_id: "message-second", + }; + const earlier = order === "FIFO" ? first : second; + const later = order === "FIFO" ? second : first; + const waitingForHead = { + ...enqueuePendingChatTask(undefined, earlier, true), + supports_queue: true, + }; + + const result = enqueuePendingChatTask(waitingForHead, later, true); + + expect(result.task_id).toBeUndefined(); + expect(result.supports_queue).toBe(true); + expect(result.queued_tasks).toEqual([first, second]); + expect(waitingForHead.queued_tasks).toEqual([earlier]); + + const head = task("head", "2026-01-01T00:00:00Z"); + expect(enqueuePendingChatTask(result, head, false)).toEqual({ + ...head, + supports_queue: true, + queued_tasks: [first, second], + }); + }, + ); + + it.each(["rich-first", "sparse-first"])( + "deduplicates headless follow-ups without losing previews or siblings (%s)", + (order) => { + const rich = { + ...task("first", "2026-01-01T00:00:01Z"), + message_id: "message-first", + }; + const sparse = { + task_id: rich.task_id, + status: rich.status, + created_at: rich.created_at, + }; + const sibling = { + ...task("second", "2026-01-01T00:00:02Z"), + message_id: "message-second", + }; + const initial = order === "rich-first" ? rich : sparse; + const duplicate = order === "rich-first" ? sparse : rich; + const waitingForHead = enqueuePendingChatTask(undefined, initial, true); + const withSibling = enqueuePendingChatTask(waitingForHead, sibling, true); + + const result = enqueuePendingChatTask(withSibling, duplicate, true); + + expect(result.task_id).toBeUndefined(); + expect(result.queued_tasks).toEqual([rich, sibling]); + }, + ); + it("preserves a follow-up when its response arrives before the head response", () => { const followUp = { ...task("next", "2026-01-01T00:00:01Z"), diff --git a/packages/core/chat/pending.ts b/packages/core/chat/pending.ts index 7a499e92dc2..95adbc6f596 100644 --- a/packages/core/chat/pending.ts +++ b/packages/core/chat/pending.ts @@ -47,7 +47,12 @@ export function enqueuePendingChatTask( ? {} : { supports_queue: current.supports_queue }; return queued - ? { ...queueSupport, queued_tasks: [task] } + ? { + ...queueSupport, + // The head may still be loading when several follow-ups settle. + // Accumulate their acknowledgements instead of replacing the queue. + queued_tasks: normalizeQueue([...(current?.queued_tasks ?? []), task]), + } : { ...task, ...queueSupport,