fix(session): prevent double auto-compaction from filterCompacted reorder (#27545)

This commit is contained in:
Kit Langton
2026-05-14 13:56:12 -04:00
committed by GitHub
parent 855bda8384
commit 94564f3588
3 changed files with 135 additions and 13 deletions

View File

@@ -1547,3 +1547,110 @@ describe("session.message-v2.fromError", () => {
expect(result.name).toBe("MessageAbortedError")
})
})
describe("session.message-v2.latest", () => {
const TAIL_USER = MessageID.make("msg_001")
const OVERFLOW_ASSISTANT = MessageID.make("msg_002")
const COMPACTION_USER = MessageID.make("msg_003")
const SUMMARY_ASSISTANT = MessageID.make("msg_004")
const CONTINUE_USER = MessageID.make("msg_005")
const NEW_COMPACTION_USER = MessageID.make("msg_006")
const tailUser: MessageV2.WithParts = {
info: userInfo(TAIL_USER),
parts: [{ ...basePart(TAIL_USER, "p1"), type: "text", text: "original prompt" }] as MessageV2.Part[],
}
const overflowAssistant: MessageV2.WithParts = {
info: {
...assistantInfo(OVERFLOW_ASSISTANT, TAIL_USER),
finish: "tool-calls",
tokens: { input: 280_000, output: 200, reasoning: 0, cache: { read: 0, write: 0 }, total: 280_200 },
} as MessageV2.Assistant,
parts: [],
}
const compactionUser: MessageV2.WithParts = {
info: userInfo(COMPACTION_USER),
parts: [
{
...basePart(COMPACTION_USER, "p1"),
type: "compaction",
auto: true,
tail_start_id: TAIL_USER,
},
] as MessageV2.Part[],
}
const summaryAssistant: MessageV2.WithParts = {
info: {
...assistantInfo(SUMMARY_ASSISTANT, COMPACTION_USER),
summary: true,
finish: "stop",
tokens: { input: 150_000, output: 1_500, reasoning: 0, cache: { read: 0, write: 0 }, total: 151_500 },
} as MessageV2.Assistant,
parts: [],
}
const continueUser: MessageV2.WithParts = {
info: userInfo(CONTINUE_USER),
parts: [
{
...basePart(CONTINUE_USER, "p1"),
type: "text",
text: "Continue if you have next steps...",
synthetic: true,
metadata: { compaction_continue: true },
},
] as MessageV2.Part[],
}
// Regression for double auto-compaction. The reorder in filterCompacted
// (#27145) returns [compaction-user, summary, ...tail..., continue-user],
// so picking lastFinished by array position landed on the pre-compaction
// overflow assistant and bypassed the `summary !== true` overflow guard
// in SessionPrompt.runLoop, firing a second compaction.create immediately.
test("finished is the chronologically-latest finished assistant, not the array-latest", () => {
const filtered = MessageV2.filterCompacted([
continueUser,
summaryAssistant,
compactionUser,
overflowAssistant,
tailUser,
])
const state = MessageV2.latest(filtered)
expect(state.finished?.id).toBe(SUMMARY_ASSISTANT)
expect(state.finished?.summary).toBe(true)
expect(state.user?.id).toBe(CONTINUE_USER)
expect(state.tasks).toEqual([])
})
test("a fresh compaction-user newer than the latest summary surfaces in tasks", () => {
const newCompactionUser: MessageV2.WithParts = {
info: userInfo(NEW_COMPACTION_USER),
parts: [
{
...basePart(NEW_COMPACTION_USER, "p1"),
type: "compaction",
auto: true,
},
] as MessageV2.Part[],
}
const state = MessageV2.latest([
tailUser,
overflowAssistant,
compactionUser,
summaryAssistant,
continueUser,
newCompactionUser,
])
expect(state.finished?.id).toBe(SUMMARY_ASSISTANT)
expect(state.user?.id).toBe(NEW_COMPACTION_USER)
expect(state.tasks).toHaveLength(1)
expect(state.tasks[0]).toMatchObject({ type: "compaction", auto: true })
})
})