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)}` },