From cbb811817af71de0e05e572f1f01fd3bab17d182 Mon Sep 17 00:00:00 2001 From: dex0shubham Date: Mon, 27 Jul 2026 09:30:17 +0100 Subject: [PATCH] fix(openai-history): keepTail:0 in mixed Responses collapse protected every message planResponsesMixedCollapse computed the protected tail as messageIndices.slice(-Math.max(0, Math.floor(keepTail))). When keepTail is 0 that is slice(-0), which returns the WHOLE array, so "protect zero trailing messages" instead protected every message and blocked all history collapse (reason: no_closed_prefix, nothing imaged). keepTail:0 is a legal host override (nonNegativeInt accepts 0 via gptHistory.keepTail). Guard keepTail===0 explicitly so it protects nothing; the latest user turn is still protected separately via latestUserIndex. Co-Authored-By: Claude Opus 4.8 --- src/core/openai-history.ts | 6 +++++- tests/openai-history.test.ts | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/core/openai-history.ts b/src/core/openai-history.ts index 9b5136e0..7609c390 100644 --- a/src/core/openai-history.ts +++ b/src/core/openai-history.ts @@ -784,8 +784,12 @@ async function planResponsesMixedCollapse( messageIndices.push(i); if (msg.role === 'user') latestUserIndex = i; } + // slice(-0) returns the WHOLE array, so guard keepTail===0 explicitly — + // otherwise "protect 0 tail messages" protects every message and blocks + // all collapse. latestUserIndex is added separately below. + const tail = Math.max(0, Math.floor(o.keepTail)); const protectedMessages = new Set( - messageIndices.slice(-Math.max(0, Math.floor(o.keepTail))), + tail > 0 ? messageIndices.slice(-tail) : [], ); if (latestUserIndex >= 0) protectedMessages.add(latestUserIndex); diff --git a/tests/openai-history.test.ts b/tests/openai-history.test.ts index 60fa95e0..6a8af550 100644 --- a/tests/openai-history.test.ts +++ b/tests/openai-history.test.ts @@ -558,6 +558,25 @@ describe('planResponsesPairCollapse — native state classification', () => { expect(plan.baselineTokens).toBeLessThan(o200k(plan.text)); }); + it('mixed mode with keepTail 0 collapses old messages (no slice(-0) whole-array protect)', async () => { + // Regression: keepTail:0 must protect NO tail messages. slice(-0) returns + // the whole array, which would protect every message and block all collapse. + const items: Array> = [ + { role: 'user', content: `old request ${'alpha '.repeat(500)}` }, // 0 + { role: 'assistant', content: `old answer ${'beta '.repeat(500)}` }, // 1 + { role: 'user', content: 'current live request' }, // 2 (latest user) + ]; + const plan = await planResponsesPairCollapse(items, yes, { + responsesMode: 'mixed', keepTail: 0, keepRecentPairs: 0, + minCollapseTokens: 1, maxImages: 100, + }); + const selected = new Set(plan.selectedIndices); + // Only the latest user turn stays native; the old turns collapse. + expect(selected.has(0)).toBe(true); + expect(selected.has(1)).toBe(true); + expect(selected.has(2)).toBe(false); + }); + it('with a one-item Sol tail, protects the latest user and newest closed pair only', async () => { const items: Array> = [ { role: 'user', content: `old request ${'alpha '.repeat(500)}` },