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>
This commit is contained in:
Don Jayamanne
2026-06-18 18:27:55 +10:00
committed by GitHub
parent 2fa0bc3c62
commit 77b96cfbad
2 changed files with 40 additions and 1 deletions

View File

@@ -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;
}

View File

@@ -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 = {