fix(coding-agent): unify compaction UI events closes #2617

This commit is contained in:
Mario Zechner
2026-03-27 03:14:24 +01:00
parent 1ba899f6a6
commit 161ad18287
9 changed files with 233 additions and 105 deletions

View File

@@ -153,10 +153,10 @@ describe("AgentSession auto-compaction queue resume", () => {
)
.mockResolvedValue();
const events: Array<{ type: string; errorMessage?: string }> = [];
const events: Array<{ type: string; reason: string; errorMessage?: string }> = [];
session.subscribe((event) => {
if (event.type === "auto_compaction_end") {
events.push({ type: event.type, errorMessage: event.errorMessage });
if (event.type === "compaction_end") {
events.push({ type: event.type, reason: event.reason, errorMessage: event.errorMessage });
}
});
@@ -171,7 +171,8 @@ describe("AgentSession auto-compaction queue resume", () => {
expect(runAutoCompactionSpy).toHaveBeenCalledTimes(1);
expect(events).toContainEqual({
type: "auto_compaction_end",
type: "compaction_end",
reason: "overflow",
errorMessage:
"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.",
});

View File

@@ -181,7 +181,7 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
expect(compactionEntries.length).toBe(1);
}, 120000);
it("should emit correct events during auto-compaction", async () => {
it("should emit compaction events during manual compaction", async () => {
createSession();
// Build some history
@@ -191,12 +191,15 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
// Manually trigger compaction and check events
await session.compact();
// Check that no auto_compaction events were emitted for manual compaction
const autoCompactionEvents = events.filter(
(e) => e.type === "auto_compaction_start" || e.type === "auto_compaction_end",
);
// Manual compaction doesn't emit auto_compaction events
expect(autoCompactionEvents.length).toBe(0);
const compactionEvents = events.filter((e) => e.type === "compaction_start" || e.type === "compaction_end");
expect(compactionEvents).toHaveLength(2);
expect(compactionEvents[0]).toEqual({ type: "compaction_start", reason: "manual" });
expect(compactionEvents[1]).toMatchObject({
type: "compaction_end",
reason: "manual",
aborted: false,
willRetry: false,
});
// Regular events should have been emitted
const messageEndEvents = events.filter((e) => e.type === "message_end");

View File

@@ -0,0 +1,50 @@
import { describe, expect, test, vi } from "vitest";
import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js";
describe("InteractiveMode compaction events", () => {
test("rebuilds chat without appending a duplicate compaction summary", async () => {
const fakeThis = {
isInitialized: true,
footer: { invalidate: vi.fn() },
autoCompactionEscapeHandler: undefined as (() => void) | undefined,
autoCompactionLoader: undefined,
defaultEditor: {},
statusContainer: { clear: vi.fn() },
chatContainer: { clear: vi.fn() },
rebuildChatFromMessages: vi.fn(),
addMessageToChat: vi.fn(),
showError: vi.fn(),
showStatus: vi.fn(),
flushCompactionQueue: vi.fn().mockResolvedValue(undefined),
ui: { requestRender: vi.fn() },
};
const handleEvent = Reflect.get(InteractiveMode.prototype, "handleEvent") as (
this: typeof fakeThis,
event: {
type: "compaction_end";
reason: "manual" | "threshold" | "overflow";
result: { tokensBefore: number; summary: string } | undefined;
aborted: boolean;
willRetry: boolean;
errorMessage?: string;
},
) => Promise<void>;
await handleEvent.call(fakeThis, {
type: "compaction_end",
reason: "manual",
result: {
tokensBefore: 123,
summary: "summary",
},
aborted: false,
willRetry: false,
});
expect(fakeThis.chatContainer.clear).toHaveBeenCalledTimes(1);
expect(fakeThis.rebuildChatFromMessages).toHaveBeenCalledTimes(1);
expect(fakeThis.addMessageToChat).not.toHaveBeenCalled();
expect(fakeThis.flushCompactionQueue).toHaveBeenCalledWith({ willRetry: false });
});
});

View File

@@ -0,0 +1,56 @@
import { describe, expect, test, vi } from "vitest";
import triggerCompactExtension from "../examples/extensions/trigger-compact.js";
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../src/core/extensions/index.js";
function createContext(tokens: number | null, compact = vi.fn()): ExtensionContext {
return {
hasUI: false,
ui: {} as ExtensionContext["ui"],
cwd: process.cwd(),
sessionManager: {} as ExtensionContext["sessionManager"],
modelRegistry: {} as ExtensionContext["modelRegistry"],
model: undefined,
isIdle: () => true,
abort: vi.fn(),
hasPendingMessages: () => false,
shutdown: vi.fn(),
getContextUsage: () => ({ tokens, contextWindow: 200_000, percent: tokens === null ? null : tokens / 2000 }),
compact,
getSystemPrompt: () => "",
};
}
describe("trigger-compact example extension", () => {
test("only auto-compacts when context usage crosses the threshold", () => {
let turnEndHandler:
| ((event: { type: "turn_end" }, ctx: ExtensionContext | ExtensionCommandContext) => void)
| undefined;
const api = {
on: (event: string, handler: (event: { type: "turn_end" }, ctx: ExtensionContext) => void) => {
if (event === "turn_end") {
turnEndHandler = handler;
}
},
registerCommand: vi.fn(),
} as unknown as ExtensionAPI;
triggerCompactExtension(api);
expect(turnEndHandler).toBeDefined();
const compact = vi.fn();
const event = { type: "turn_end" } as const;
turnEndHandler?.(event, createContext(110_000, compact));
expect(compact).not.toHaveBeenCalled();
turnEndHandler?.(event, createContext(120_000, compact));
expect(compact).not.toHaveBeenCalled();
turnEndHandler?.(event, createContext(95_000, compact));
expect(compact).not.toHaveBeenCalled();
turnEndHandler?.(event, createContext(105_000, compact));
expect(compact).toHaveBeenCalledTimes(1);
});
});