From f2ee18f033ef11005d0615ae6fc108e1b594828d Mon Sep 17 00:00:00 2001 From: jatmn Date: Mon, 6 Jul 2026 17:03:23 -0700 Subject: [PATCH 1/2] fix(compact): count string message content --- src/services/compact/microCompact.test.ts | 73 +++++++++++++++++++++++ src/services/compact/microCompact.ts | 5 ++ 2 files changed, 78 insertions(+) diff --git a/src/services/compact/microCompact.test.ts b/src/services/compact/microCompact.test.ts index f46b339104..d36b8461ca 100644 --- a/src/services/compact/microCompact.test.ts +++ b/src/services/compact/microCompact.test.ts @@ -175,3 +175,76 @@ describe('microCompact MCP tool compaction', () => { }) }) }) + +describe('estimateMessageTokens coverage', () => { + test('counts tokens from string content user messages', async () => { + const { estimateMessageTokens } = await import('./microCompact.js') + + // Real user messages from the session can have string content (plain user prompt) + const messages: Message[] = [ + createUserMessage({ + content: + 'Hello world this is a long user prompt that needs compression because it is over the threshold', + }), + createAssistantMessage({ + content: [ + { + type: 'text', + text: 'Hi there! I am happy to help you with your request today', + citations: [], + }, + ], + }), + ] + + const total = estimateMessageTokens(messages) + // The string-content user message should be counted alongside normal blocks. + expect(total).toBeGreaterThan(0) + }) + + test('counts tokens across all block types including tool_result and tool_use', async () => { + const { estimateMessageTokens } = await import('./microCompact.js') + + const msg = createUserMessage({ + content: [ + { type: 'tool_result' as const, tool_use_id: 't1', content: 'A'.repeat(5000) }, + ], + }) + const msgWithText = createUserMessage({ + content: [ + { type: 'text' as const, text: 'User query here' }, + ], + }) + const asstTool = createAssistantMessage({ + content: [ + { + type: 'tool_use' as const, + id: 't1', + name: 'Bash', + input: { command: 'ls -la' }, + }, + ], + }) + const msgWithResultAndText = createUserMessage({ + content: [ + { type: 'tool_result' as const, tool_use_id: 't1', content: 'file output data' }, + { type: 'text' as const, text: 'based on that output I think the answer is yes' }, + ], + }) + + const messages: Message[] = [msg, msgWithText, asstTool, msgWithResultAndText] + + const total = estimateMessageTokens(messages) + + // Must be significantly > 0 (all blocks counted) + expect(total).toBeGreaterThan(100) + + // Must be larger than the old reducer's estimate (only string+text blocks) + // Old reducer would only see text blocks: 'User query here' + 'based on that output...' + // It would miss 5000-chars tool_result, tool_use name+input + const oldReducerEstimate = Math.ceil( + (13 + 46) * (4 / 3), // 'User query here' (13) + 'based on that...' (~46) + ) + expect(total).toBeGreaterThan(oldReducerEstimate * 2) + }) +}) diff --git a/src/services/compact/microCompact.ts b/src/services/compact/microCompact.ts index eaa92eda97..84531d1c53 100644 --- a/src/services/compact/microCompact.ts +++ b/src/services/compact/microCompact.ts @@ -168,6 +168,11 @@ export function estimateMessageTokens(messages: Message[]): number { continue } + if (typeof message.message?.content === 'string') { + totalTokens += roughTokenCountEstimation(message.message.content) + continue + } + if (!Array.isArray(message.message.content)) { continue } From c4b9906b2878130613f50205555336a69b417ca3 Mon Sep 17 00:00:00 2001 From: jatmn Date: Mon, 6 Jul 2026 17:24:03 -0700 Subject: [PATCH 2/2] test(compact): assert string content token estimate --- src/services/compact/microCompact.test.ts | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/services/compact/microCompact.test.ts b/src/services/compact/microCompact.test.ts index d36b8461ca..f4493973ee 100644 --- a/src/services/compact/microCompact.test.ts +++ b/src/services/compact/microCompact.test.ts @@ -181,25 +181,14 @@ describe('estimateMessageTokens coverage', () => { const { estimateMessageTokens } = await import('./microCompact.js') // Real user messages from the session can have string content (plain user prompt) - const messages: Message[] = [ - createUserMessage({ - content: - 'Hello world this is a long user prompt that needs compression because it is over the threshold', - }), - createAssistantMessage({ - content: [ - { - type: 'text', - text: 'Hi there! I am happy to help you with your request today', - citations: [], - }, - ], - }), - ] + const content = + 'Hello world this is a long user prompt that needs compression because it is over the threshold' + const messages: Message[] = [createUserMessage({ content })] const total = estimateMessageTokens(messages) - // The string-content user message should be counted alongside normal blocks. - expect(total).toBeGreaterThan(0) + // The string-content user message should be counted on its own; the old + // implementation returned 0 because it skipped non-array content. + expect(total).toBe(Math.ceil(Math.round(content.length / 4) * (4 / 3))) }) test('counts tokens across all block types including tool_result and tool_use', async () => {