diff --git a/src/agents/embedded-agent-runner.limithistoryturns.test.ts b/src/agents/embedded-agent-runner.limithistoryturns.test.ts index daede5f422b..5d3df657c6b 100644 --- a/src/agents/embedded-agent-runner.limithistoryturns.test.ts +++ b/src/agents/embedded-agent-runner.limithistoryturns.test.ts @@ -118,6 +118,50 @@ describe("limitHistoryTurns", () => { expect(limited[1].role).toBe("assistant"); }); + it("preserves leading compactionSummary when limiting", () => { + const compactionSummary: AgentMessage = { + role: "compactionSummary", + summary: "Previous conversation about topic X", + tokensBefore: 5000, + tokensAfter: 2000, + timestamp: Date.now(), + } as AgentMessage; + const messages = [ + compactionSummary, + ...makeMessages(["user", "assistant", "user", "assistant"]), + ]; + const limited = limitHistoryTurns(messages, 1); + // compactionSummary is preserved, last 1 user turn + assistant kept + expect(limited.length).toBe(3); + expect(limited[0].role).toBe("compactionSummary"); + expect(firstText(limited[1])).toBe("message 2"); + }); + + it("preserves leading branchSummary when limiting", () => { + const branchSummary: AgentMessage = { + role: "branchSummary", + summary: "Branch context", + fromId: "abc", + timestamp: Date.now(), + } as AgentMessage; + const messages = [branchSummary, ...makeMessages(["user", "assistant", "user", "assistant"])]; + const limited = limitHistoryTurns(messages, 1); + expect(limited.length).toBe(3); + expect(limited[0].role).toBe("branchSummary"); + }); + + it("returns all when only non-conversation messages exist", () => { + const compactionSummary: AgentMessage = { + role: "compactionSummary", + summary: "Summary only", + tokensBefore: 1000, + timestamp: Date.now(), + } as AgentMessage; + const limited = limitHistoryTurns([compactionSummary], 2); + expect(limited).toHaveLength(1); + expect(limited[0].role).toBe("compactionSummary"); + }); + it("preserves message content integrity", () => { // Limiting should slice whole turns, not mutate tool calls or message bodies. const messages: AgentMessage[] = [ diff --git a/src/agents/embedded-agent-runner/history.ts b/src/agents/embedded-agent-runner/history.ts index 936e833d6a2..59b53317a4c 100644 --- a/src/agents/embedded-agent-runner/history.ts +++ b/src/agents/embedded-agent-runner/history.ts @@ -16,6 +16,10 @@ function stripThreadSuffix(value: string): string { /** * Limits conversation history to the last N user turns (and their associated * assistant responses). This reduces token usage for long-running DM sessions. + * + * Leading non-conversation messages (e.g. compactionSummary, branchSummary) + * placed at index 0 by buildSessionContext are always preserved, since they + * carry summarized pre-compaction context that history limiting must not drop. */ export function limitHistoryTurns( messages: AgentMessage[], @@ -25,14 +29,30 @@ export function limitHistoryTurns( return messages; } - let userCount = 0; - let lastUserIndex = messages.length; + // Preserve leading non-conversation messages (compactionSummary, branchSummary, etc.) + // that buildSessionContext places at index 0 to carry pre-compaction context. + let conversationStart = 0; + while (conversationStart < messages.length) { + const role = messages[conversationStart].role; + if (role === "user" || role === "assistant") { + break; + } + conversationStart++; + } - for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i].role === "user") { + const tail = messages.slice(conversationStart); + if (tail.length === 0) { + return messages; + } + + let userCount = 0; + let lastUserIndex = tail.length; + + for (let i = tail.length - 1; i >= 0; i--) { + if (tail[i].role === "user") { userCount++; if (userCount > limit) { - return messages.slice(lastUserIndex); + return [...messages.slice(0, conversationStart), ...tail.slice(lastUserIndex)]; } lastUserIndex = i; }