Skip to content

Commit a63afd8

Browse files
fix(agents): only deduplicate identical consecutive user messages
Previous version collapsed ALL consecutive user messages. Now only deduplicates when content is identical (fingerprint match), preserving distinct multi-message sends.
1 parent 42c03f9 commit a63afd8

2 files changed

Lines changed: 21 additions & 8 deletions

File tree

src/agents/openai-ws-stream.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -640,11 +640,9 @@ describe("convertMessagesToInputItems", () => {
640640
userMsg("Also, one more thing"),
641641
] as Parameters<typeof convertMessagesToInputItems>[0]);
642642

643-
// Consecutive but different — collapsed to last (this is acceptable;
644-
// legitimate multi-sends are rare and the model sees the latest message)
643+
// Different content — both preserved
645644
const userItems = items.filter((i) => "role" in i && i.role === "user");
646-
expect(userItems).toHaveLength(1);
647-
expect(userItems[0]).toMatchObject({ content: "Also, one more thing" });
645+
expect(userItems).toHaveLength(2);
648646
});
649647

650648
it("does not collapse user messages separated by assistant messages", () => {

src/agents/openai-ws-stream.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,11 @@ export function convertMessagesToInputItems(
533533
/**
534534
* When model fallback retries inject duplicate user messages and the
535535
* intervening error-assistant entries are dropped (empty content), the input
536-
* ends up with consecutive user messages. Collapse each run down to the last
537-
* entry so the model sees the prompt exactly once.
536+
* ends up with consecutive user messages. Deduplicate by removing consecutive
537+
* user messages with identical text content, keeping the last occurrence.
538+
*
539+
* Distinct consecutive user messages (e.g. user sends two different messages
540+
* before the model responds) are preserved.
538541
*/
539542
function collapseConsecutiveUserMessages(items: InputItem[]): InputItem[] {
540543
if (items.length <= 1) return items;
@@ -546,9 +549,9 @@ function collapseConsecutiveUserMessages(items: InputItem[]): InputItem[] {
546549
"role" in prev &&
547550
prev.role === "user" &&
548551
"role" in item &&
549-
item.role === "user"
552+
item.role === "user" &&
553+
inputItemTextFingerprint(prev) === inputItemTextFingerprint(item)
550554
) {
551-
// Replace previous with current (keep the later/more recent one)
552555
out[out.length - 1] = item;
553556
continue;
554557
}
@@ -557,6 +560,18 @@ function collapseConsecutiveUserMessages(items: InputItem[]): InputItem[] {
557560
return out;
558561
}
559562

563+
/** Extract a comparable text fingerprint from a user input item. */
564+
function inputItemTextFingerprint(item: InputItem): string {
565+
if (!("content" in item)) return "";
566+
const content = (item as { content?: unknown }).content;
567+
if (typeof content === "string") return content;
568+
if (!Array.isArray(content)) return "";
569+
return content
570+
.filter((c: { type?: string }) => c.type === "input_text")
571+
.map((c: { text?: string }) => c.text ?? "")
572+
.join("\n");
573+
}
574+
560575
// ─────────────────────────────────────────────────────────────────────────────
561576
// Response object → AssistantMessage
562577
// ─────────────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)