Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/cli/src/acp-integration/session/Session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44273,8 +44273,10 @@ describe('Session', () => {
it.each([
core.CompressionStatus.COMPRESSION_FAILED_EMPTY_SUMMARY,
core.CompressionStatus.COMPRESSION_FAILED_API_ERROR,
core.CompressionStatus.COMPRESSION_FAILED_INPUT_TOO_LARGE,
core.CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
])(
'does not count a failed Guard compression status %s or block later automatic work',
'does not count failed Guard compression status %s or block later automatic work',
async (compressionStatus) => {
rebuildSessionWithGuard();
installPendingTodoTool();
Expand Down
52 changes: 46 additions & 6 deletions packages/core/src/core/llm-chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5677,15 +5677,15 @@ describe('LlmChat', async () => {
);
});

it('triggers cache-sharing compaction end-to-end when a provider token count is available (R3.4)', async () => {
it('persists estimated cache-sharing compaction end-to-end (R3.4)', async () => {
// Reviewer R3.4: the "forwards the pending user message" test above
// mocks the service entirely, so the real cheap-gate never runs there.
// Exercise the full chain here with the provider token-count anchor
// required for cache sharing:
// sendMessageStream → tryCompress → service.compress (REAL) →
// cheap-gate (count-based estimate from the 172K anchor) →
// splitter (real) → cache-sharing request (mocked at baseLlmClient) →
// persistence.
// estimated visible-history accounting → persistence.
const largeChars = 'x'.repeat(688_000); // ~172K estimated tokens
const inheritedHistory: Content[] = [
{ role: 'user', parts: [{ text: largeChars }] },
Expand Down Expand Up @@ -5732,10 +5732,15 @@ describe('LlmChat', async () => {
expect(compressed).toBeDefined();
expect(
(compressed as { type: StreamEventType; info: ChatCompressionInfo })
.info.compressionStatus,
).toBe(CompressionStatus.COMPRESSED);
.info,
).toEqual(
expect.objectContaining({
compressionStatus: CompressionStatus.COMPRESSED,
newTokenCountIsEstimated: true,
}),
);
// Google GenAI uses the cache-sharing request rather than the cold side
// query, while still exercising the real splitter and accounting path.
// query, while still exercising the real splitter and local-delta path.
expect(generateText).toHaveBeenCalled();
expect(coldSpy).not.toHaveBeenCalled();
});
Expand Down Expand Up @@ -21120,6 +21125,33 @@ describe('LlmChat', async () => {
expect(compressSpy.mock.calls[0][1].consecutiveFailures).toBe(1);
});

it('counts an input-too-large admission rejection as a compression failure', async () => {
const compressSpy = vi
.spyOn(ChatCompressionService.prototype, 'compress')
.mockResolvedValueOnce({
newHistory: null,
info: {
originalTokenCount: 1_000,
newTokenCount: 1_000,
compressionStatus:
CompressionStatus.COMPRESSION_FAILED_INPUT_TOO_LARGE,
},
})
.mockResolvedValueOnce({
newHistory: null,
info: {
originalTokenCount: 0,
newTokenCount: 0,
compressionStatus: CompressionStatus.NOOP,
},
});

await chat.tryCompress('input-too-large');
await chat.tryCompress('after-input-too-large');

expect(compressSpy.mock.calls[1][1].consecutiveFailures).toBe(1);
});

it('forwards force=true to the compression service', async () => {
const compressSpy = mockCompressionService('compressed');

Expand Down Expand Up @@ -21341,16 +21373,24 @@ describe('LlmChat', async () => {
expect(compressSpy.mock.calls[0][1].originalTokenCount).not.toBe(
adjustedAfterFast,
);
expect(compressSpy).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ originalTokenCountIsEstimated: true }),
);
expect(info.originalTokenCountIsEstimated).toBe(true);
});

it('reports an authoritative original count when the API count is fresh', async () => {
mockCompressionService('compressed');
const compressSpy = mockCompressionService('compressed');
chat.setHistory([userMsg('a'), modelMsg('b')]);
chat.seedResumeTokenCounts(5000, 0, false);

const info = await chat.tryCompress('p-authoritative-original', true);

expect(compressSpy).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ originalTokenCountIsEstimated: false }),
);
expect(info.originalTokenCountIsEstimated).toBe(false);
});

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/core/llm-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2669,6 +2669,7 @@ export class LlmChat {
config: this.config,
consecutiveFailures: this.consecutiveFailures,
originalTokenCount,
originalTokenCountIsEstimated,
pendingUserMessage: options?.pendingUserMessage,
precomputedEffectiveTokens: options?.precomputedEffectiveTokens,
requestGenerationConfig: options?.requestGenerationConfig,
Expand Down
53 changes: 15 additions & 38 deletions packages/core/src/core/turn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,46 +56,23 @@ vi.mock('../utils/errorReporting', () => ({
}));

describe('isCompressionFailureStatus', () => {
it('treats each compression failure status as failed', () => {
expect(
isCompressionFailureStatus(
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
),
).toBe(true);
expect(
isCompressionFailureStatus(
CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
),
).toBe(true);
expect(
isCompressionFailureStatus(
CompressionStatus.COMPRESSION_FAILED_EMPTY_SUMMARY,
),
).toBe(true);
expect(
isCompressionFailureStatus(
CompressionStatus.COMPRESSION_FAILED_OUTPUT_TRUNCATED,
),
).toBe(true);
expect(
isCompressionFailureStatus(
CompressionStatus.COMPRESSION_FAILED_API_ERROR,
),
).toBe(true);
it.each([
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
CompressionStatus.COMPRESSION_FAILED_EMPTY_SUMMARY,
CompressionStatus.COMPRESSION_FAILED_OUTPUT_TRUNCATED,
CompressionStatus.COMPRESSION_FAILED_API_ERROR,
CompressionStatus.COMPRESSION_FAILED_INPUT_TOO_LARGE,
])('classifies %s as a compression failure', (status) => {
expect(isCompressionFailureStatus(status)).toBe(true);
});

it('keeps API errors distinct from other compression failure statuses', () => {
expect(CompressionStatus.COMPRESSION_FAILED_API_ERROR).not.toBe(
CompressionStatus.COMPRESSION_FAILED_EMPTY_SUMMARY,
);
expect(CompressionStatus.COMPRESSION_FAILED_API_ERROR).not.toBe(
CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
);
expect(isCompressionFailureStatus(CompressionStatus.COMPRESSED)).toBe(
false,
);
expect(isCompressionFailureStatus(CompressionStatus.NOOP)).toBe(false);
});
it.each([CompressionStatus.COMPRESSED, CompressionStatus.NOOP])(
'does not classify %s as a compression failure',
(status) => {
expect(isCompressionFailureStatus(status)).toBe(false);
},
);
});

describe('findRepeatedDuplicateProviderToolCall', () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/core/turn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,9 @@ export enum CompressionStatus {
* apart from model output quality failures.
*/
COMPRESSION_FAILED_API_ERROR,

/** The compression input could not leave enough room for a usable summary. */
COMPRESSION_FAILED_INPUT_TOO_LARGE,
}

export function isCompressionFailureStatus(
Expand All @@ -425,7 +428,8 @@ export function isCompressionFailureStatus(
status === CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR ||
status === CompressionStatus.COMPRESSION_FAILED_EMPTY_SUMMARY ||
status === CompressionStatus.COMPRESSION_FAILED_OUTPUT_TRUNCATED ||
status === CompressionStatus.COMPRESSION_FAILED_API_ERROR
status === CompressionStatus.COMPRESSION_FAILED_API_ERROR ||
status === CompressionStatus.COMPRESSION_FAILED_INPUT_TOO_LARGE
);
}

Expand Down
Loading
Loading