fix(agents): harden compaction and reset safety

Co-authored-by: jaden-clovervnd <91520439+jaden-clovervnd@users.noreply.github.com>
Co-authored-by: Sid <201593046+Sid-Qin@users.noreply.github.com>
Co-authored-by: Marcus Widing <245375637+widingmarcus-cyber@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-02-26 17:35:55 +01:00
parent 273973d374
commit 0ec7711bc2
20 changed files with 472 additions and 18 deletions

View File

@@ -428,3 +428,59 @@ describe("compaction-safeguard extension model fallback", () => {
expect(getApiKeyMock).not.toHaveBeenCalled();
});
});
describe("compaction-safeguard double-compaction guard", () => {
it("cancels compaction when there are no real messages to summarize", async () => {
const sessionManager = stubSessionManager();
const model = createAnthropicModelFixture();
setCompactionSafeguardRuntime(sessionManager, { model });
const compactionHandler = createCompactionHandler();
const mockEvent = {
preparation: {
messagesToSummarize: [] as AgentMessage[],
turnPrefixMessages: [] as AgentMessage[],
firstKeptEntryId: "entry-1",
tokensBefore: 1500,
fileOps: { read: [], edited: [], written: [] },
},
customInstructions: "",
signal: new AbortController().signal,
};
const getApiKeyMock = vi.fn().mockResolvedValue("sk-test");
const mockContext = createCompactionContext({
sessionManager,
getApiKeyMock,
});
const result = (await compactionHandler(mockEvent, mockContext)) as {
cancel?: boolean;
};
expect(result).toEqual({ cancel: true });
expect(getApiKeyMock).not.toHaveBeenCalled();
});
it("continues when messages include real conversation content", async () => {
const sessionManager = stubSessionManager();
const model = createAnthropicModelFixture();
setCompactionSafeguardRuntime(sessionManager, { model });
const compactionHandler = createCompactionHandler();
const mockEvent = createCompactionEvent({
messageText: "real message",
tokensBefore: 1500,
});
const getApiKeyMock = vi.fn().mockResolvedValue(null);
const mockContext = createCompactionContext({
sessionManager,
getApiKeyMock,
});
const result = (await compactionHandler(mockEvent, mockContext)) as {
cancel?: boolean;
};
expect(result).toEqual({ cancel: true });
expect(getApiKeyMock).toHaveBeenCalled();
});
});

View File

@@ -130,6 +130,10 @@ function formatToolFailuresSection(failures: ToolFailure[]): string {
return `\n\n## Tool Failures\n${lines.join("\n")}`;
}
function isRealConversationMessage(message: AgentMessage): boolean {
return message.role === "user" || message.role === "assistant" || message.role === "toolResult";
}
function computeFileLists(fileOps: FileOperations): {
readFiles: string[];
modifiedFiles: string[];
@@ -191,6 +195,12 @@ async function readWorkspaceContextForSummary(): Promise<string> {
export default function compactionSafeguardExtension(api: ExtensionAPI): void {
api.on("session_before_compact", async (event, ctx) => {
const { preparation, customInstructions, signal } = event;
if (!preparation.messagesToSummarize.some(isRealConversationMessage)) {
log.warn(
"Compaction safeguard: cancelling compaction with no real conversation messages to summarize.",
);
return { cancel: true };
}
const { readFiles, modifiedFiles } = computeFileLists(preparation.fileOps);
const fileOpsSummary = formatFileOperations(readFiles, modifiedFiles);
const toolFailures = collectToolFailures([