From 77b96cfbade9f366ded95d6747a4c8135a9355ef Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 18 Jun 2026 18:27:55 +1000 Subject: [PATCH] agentHost: handle no-op Copilot compaction errors (#321875) Handle no-op Copilot compaction errors (#321827) Treat Copilot CLI history compact errors containing "nothing to compact" as a successful no-op so /compact completes with the standard completion message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../node/copilot/copilotAgentSession.ts | 7 +++- .../test/node/copilotAgentSession.test.ts | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts index 8f4ef09561d..0a585bb20c0 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts @@ -7,7 +7,7 @@ import type { CopilotSession, ExitPlanModeRequest, MessageOptions, PermissionReq import { DeferredPromise } from '../../../../base/common/async.js'; import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js'; import { Emitter } from '../../../../base/common/event.js'; -import { CancellationError } from '../../../../base/common/errors.js'; +import { CancellationError, getErrorMessage } from '../../../../base/common/errors.js'; import { escapeMarkdownSyntaxTokens } from '../../../../base/common/htmlContent.js'; import { Disposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; @@ -1000,6 +1000,11 @@ export class CopilotAgentSession extends Disposable { await this._wrapper.session.rpc.history.compact(); this.emitInitialMarkdown(localize('copilotAgent.compactionCompleted', "Compaction completed")); } catch (err) { + if (getErrorMessage(err).toLowerCase().includes('nothing to compact')) { + this.emitInitialMarkdown(localize('copilotAgent.compactionCompleted', "Compaction completed")); + this._completeActiveTurn(); + return; + } this._logService.error(err, `[Copilot:${this.sessionId}] rpc.history.compact failed`); throw err; } diff --git a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts index c5797762ed1..5b934e12887 100644 --- a/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts @@ -50,6 +50,7 @@ class MockCopilotSession { readonly commandListCalls: unknown[] = []; readonly commandInvokeCalls: Array<{ name: string; input?: string }> = []; compactResult: { success: boolean; tokensRemoved: number; messagesRemoved: number } = { success: true, tokensRemoved: 0, messagesRemoved: 0 }; + compactError: unknown = undefined; commandListResult: { commands: Array<{ name: string; kind: 'builtin' | 'skill' | 'client'; description: string; allowDuringAgentExecution: boolean }> } = { commands: [] }; commandInvokeResult: { kind: 'text'; text: string; markdown?: boolean } | { kind: 'completed'; message?: string } | { kind: 'agent-prompt'; prompt: string; displayPrompt: string; mode?: 'interactive' | 'plan' | 'autopilot' } = { kind: 'text', text: '' }; messages: SessionEvent[] = []; @@ -103,6 +104,9 @@ class MockCopilotSession { history: { compact: async (params?: unknown) => { this.compactCalls.push(params ?? null); + if (this.compactError !== undefined) { + throw this.compactError; + } return this.compactResult; }, }, @@ -633,6 +637,36 @@ suite('CopilotAgentSession', () => { assert.ok(turnComplete, 'expected the turn to complete on a failed compaction'); }); + test('`/compact` treats nothing-to-compact errors as completed', async () => { + const logService = new CapturingLogService(); + const { session, mockSession, signals } = await createAgentSession(disposables, { logService }); + mockSession.compactError = new Error('NOTHING TO COMPACT for this conversation'); + + await session.send('/compact', undefined, 'turn-compact'); + + const actions = getActions(signals); + assert.deepStrictEqual({ + compactCalls: mockSession.compactCalls.length, + sendRequests: mockSession.sendRequests, + errors: logService.errors, + responseParts: actions + .filter(a => a.type === ActionType.ChatResponsePart) + .map(a => { + const part = (a as ChatResponsePartAction).part; + return part.kind === ResponsePartKind.Markdown ? { turnId: a.turnId, kind: part.kind, content: part.content } : { turnId: a.turnId, kind: part.kind }; + }), + turnComplete: actions + .filter(a => a.type === ActionType.ChatTurnComplete) + .map(a => (a as ChatTurnCompleteAction).turnId), + }, { + compactCalls: 1, + sendRequests: [], + errors: [], + responseParts: [{ turnId: 'turn-compact', kind: ResponsePartKind.Markdown, content: 'Compaction completed' }], + turnComplete: ['turn-compact'], + }); + }); + test('`/env` runs the runtime command when listed and emits markdown output', async () => { const { session, mockSession, signals } = await createAgentSession(disposables); mockSession.commandListResult = {