fix(agent): protect bootstrap prefix from pruning

This commit is contained in:
Max Sumrall
2026-01-07 18:15:54 +01:00
committed by Peter Steinberger
parent bf00b733c9
commit 5ddf9b2c65
3 changed files with 54 additions and 2 deletions

View File

@@ -141,6 +141,43 @@ describe("context-pruning", () => {
expect(toolText(findToolResult(next, "t4"))).toContain("w".repeat(20_000));
expect(toolText(findToolResult(next, "t1"))).toBe("[cleared]");
});
it("never prunes tool results before the first user message", () => {
const settings = computeEffectiveSettings({
mode: "aggressive",
keepLastAssistants: 0,
hardClear: { placeholder: "[cleared]" },
});
if (!settings) throw new Error("expected settings");
const messages: AgentMessage[] = [
makeAssistant("bootstrap tool calls"),
makeToolResult({
toolCallId: "t0",
toolName: "read",
text: "x".repeat(20_000),
}),
makeAssistant("greeting"),
makeUser("u1"),
makeToolResult({
toolCallId: "t1",
toolName: "bash",
text: "y".repeat(20_000),
}),
];
const next = pruneContextMessages({
messages,
settings,
ctx: { model: { contextWindow: 1000 } } as unknown as ExtensionContext,
isToolPrunable: () => true,
contextWindowTokensOverride: 1000,
});
expect(toolText(findToolResult(next, "t0"))).toBe("x".repeat(20_000));
expect(toolText(findToolResult(next, "t1"))).toBe("[cleared]");
});
it("mode aggressive clears eligible tool results before cutoff", () => {
const messages: AgentMessage[] = [
makeUser("u1"),

View File

@@ -153,6 +153,13 @@ function findAssistantCutoffIndex(
return null;
}
function findFirstUserIndex(messages: AgentMessage[]): number | null {
for (let i = 0; i < messages.length; i++) {
if (messages[i]?.role === "user") return i;
}
return null;
}
function softTrimToolResultMessage(params: {
msg: ToolResultMessage;
settings: EffectiveContextPruningSettings;
@@ -207,13 +214,20 @@ export function pruneContextMessages(params: {
);
if (cutoffIndex === null) return messages;
// Bootstrap safety: never prune anything before the first user message. This protects initial
// "identity" reads (SOUL.md, USER.md, etc.) which typically happen before the first inbound user
// message exists in the session transcript.
const firstUserIndex = findFirstUserIndex(messages);
const pruneStartIndex =
firstUserIndex === null ? messages.length : firstUserIndex;
const isToolPrunable =
params.isToolPrunable ?? makeToolPrunablePredicate(settings.tools);
if (settings.mode === "aggressive") {
let next: AgentMessage[] | null = null;
for (let i = 0; i < cutoffIndex; i++) {
for (let i = pruneStartIndex; i < cutoffIndex; i++) {
const msg = messages[i];
if (!msg || msg.role !== "toolResult") continue;
if (!isToolPrunable(msg.toolName)) continue;
@@ -248,7 +262,7 @@ export function pruneContextMessages(params: {
const prunableToolIndexes: number[] = [];
let next: AgentMessage[] | null = null;
for (let i = 0; i < cutoffIndex; i++) {
for (let i = pruneStartIndex; i < cutoffIndex; i++) {
const msg = messages[i];
if (!msg || msg.role !== "toolResult") continue;
if (!isToolPrunable(msg.toolName)) continue;