From d6cd835c3ed61ec48b4bc1e8c02c289df11b68d4 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Fri, 29 May 2026 17:21:23 +0800 Subject: [PATCH] fix: recover from model token limit errors --- .changeset/retry-model-token-limit.md | 7 ++++ .../agent-core/src/agent/compaction/full.ts | 27 +++++++++---- .../agent-core/test/agent/compaction.test.ts | 39 +++++++++++++++++++ packages/kosong/src/errors.ts | 1 + packages/kosong/test/errors.test.ts | 1 + 5 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 .changeset/retry-model-token-limit.md diff --git a/.changeset/retry-model-token-limit.md b/.changeset/retry-model-token-limit.md new file mode 100644 index 0000000000..221a6d4160 --- /dev/null +++ b/.changeset/retry-model-token-limit.md @@ -0,0 +1,7 @@ +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kosong": patch +"@moonshot-ai/kimi-code": patch +--- + +Recover from provider model token limit errors during long conversations. diff --git a/packages/agent-core/src/agent/compaction/full.ts b/packages/agent-core/src/agent/compaction/full.ts index 34c699aa06..47925385c9 100644 --- a/packages/agent-core/src/agent/compaction/full.ts +++ b/packages/agent-core/src/agent/compaction/full.ts @@ -46,6 +46,7 @@ export class FullCompaction { startedAt: number; telemetryTrigger: CompactionTelemetryTrigger; promise: Promise; + blockedByTurn: boolean; } | null = null; protected _compactedHistory: CompactedHistory[] = []; protected readonly strategy: CompactionStrategy; @@ -112,6 +113,7 @@ export class FullCompaction { startedAt: Date.now(), telemetryTrigger: compactionTelemetryTrigger(data.source, data.instruction), promise: Promise.resolve(), + blockedByTurn: false, }; this.compacting = active; active.promise = this.compactionWorker(abortController.signal, data, compactedCount); @@ -195,6 +197,7 @@ export class FullCompaction { private async block(signal: AbortSignal): Promise { const active = this.compacting; if (active) { + active.blockedByTurn = true; signal.addEventListener('abort', () => { if (this.compacting === active) { this.cancel(); @@ -312,19 +315,23 @@ export class FullCompaction { this.triggerPostCompactHook(data, result); } catch (error) { if (!isAbortError(error)) { + const active = this.compacting; + const blockedByTurn = active?.blockedByTurn === true; this.agent.log.error('compaction failed', { code: isKimiError(error) ? error.code : undefined, error, }); this.markCanceled(); - const payload = - isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED - ? toKimiErrorPayload(error) - : makeErrorPayload(ErrorCodes.COMPACTION_FAILED, String(error)); - this.agent.emitEvent({ - type: 'error', - ...payload, - }); + if (!blockedByTurn) { + const payload = + isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED + ? toKimiErrorPayload(error) + : makeErrorPayload(ErrorCodes.COMPACTION_FAILED, String(error)); + this.agent.emitEvent({ + type: 'error', + ...payload, + }); + } this.agent.telemetry.track('compaction_failed', { trigger_type: compactionTelemetryTrigger(data.source, data.instruction), before_tokens: tokensBefore, @@ -332,6 +339,10 @@ export class FullCompaction { retry_count: retryCount, error_type: error instanceof Error ? error.name : 'Unknown', }); + if (blockedByTurn) { + if (isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED) throw error; + throw new KimiError(ErrorCodes.COMPACTION_FAILED, String(error), { cause: error }); + } } } } diff --git a/packages/agent-core/test/agent/compaction.test.ts b/packages/agent-core/test/agent/compaction.test.ts index 87e955ae3a..cf02938c82 100644 --- a/packages/agent-core/test/agent/compaction.test.ts +++ b/packages/agent-core/test/agent/compaction.test.ts @@ -636,6 +636,45 @@ describe('Agent compaction', () => { await ctx.expectResumeMatches(); }); + it('fails a blocked turn when auto compaction generation fails', async () => { + let attempts = 0; + const generate: GenerateFn = async () => { + attempts += 1; + throw new APIStatusError(400, 'Bad request'); + }; + const ctx = testAgent({ generate, compactionStrategy: alwaysCompactOnce }); + ctx.configure(); + + await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Trigger failed auto compaction' }] }); + const events = await ctx.untilTurnEnd(); + + expect(attempts).toBe(1); + expect(events).not.toContainEqual(expect.objectContaining({ event: 'error' })); + expect(events).toContainEqual( + expect.objectContaining({ + event: 'turn.ended', + args: { + turnId: 0, + reason: 'failed', + error: expect.objectContaining({ + code: 'compaction.failed', + message: 'APIStatusError: Bad request', + }), + }, + }), + ); + const errorEvents = ctx.newEvents(); + expect(errorEvents).toHaveLength(1); + expect(errorEvents[0]).toMatchObject({ + event: 'error', + args: expect.objectContaining({ + code: 'compaction.failed', + message: 'APIStatusError: Bad request', + }), + }); + await ctx.expectResumeMatches(); + }); + it('reports compaction retry_count when retryable generation failures are exhausted', async () => { vi.useFakeTimers(); const records: TelemetryRecord[] = []; diff --git a/packages/kosong/src/errors.ts b/packages/kosong/src/errors.ts index ba104a2e4d..df2ae6adb5 100644 --- a/packages/kosong/src/errors.ts +++ b/packages/kosong/src/errors.ts @@ -82,6 +82,7 @@ const CONTEXT_OVERFLOW_MESSAGE_PATTERNS = [ /(?:too many tokens.*(?:prompt|input|context)|(?:prompt|input|context).*too many tokens)/, /prompt is too long.*maximum/, /input token count.*exceeds?.*maximum number of tokens/, + /request.*exceed(?:ed|s|ing)?.*model token limit/, ] as const; export function normalizeAPIStatusError( diff --git a/packages/kosong/test/errors.test.ts b/packages/kosong/test/errors.test.ts index 414b823122..70d5ec8520 100644 --- a/packages/kosong/test/errors.test.ts +++ b/packages/kosong/test/errors.test.ts @@ -154,6 +154,7 @@ describe('normalizeAPIStatusError', () => { [422, 'Too many tokens in prompt'], [400, 'prompt is too long: 210000 tokens exceeds the maximum'], [400, 'input token count 131072 exceeds the maximum number of tokens allowed'], + [400, 'Invalid request: Your request exceeded model token limit: 262144 (requested: 274613)'], ])('normalizes %i "%s" to APIContextOverflowError', (statusCode, message) => { const error = normalizeAPIStatusError(statusCode, message, 'req-context'); expect(error).toBeInstanceOf(APIContextOverflowError);