From 169560a5574f55140da0a0e154f5682f6a5bb0ca Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 12 May 2026 10:14:54 -0500 Subject: [PATCH] fix(compaction): ignore synthetic tail turn boundaries --- packages/opencode/src/session/compaction.ts | 1 + .../opencode/test/session/compaction.test.ts | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 90381401d9..d7b36b17df 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -186,6 +186,7 @@ function turns(messages: MessageV2.WithParts[]) { const msg = messages[i] if (msg.info.role !== "user") continue if (msg.parts.some((part) => part.type === "compaction")) continue + if (msg.parts.some((part) => part.type === "text" && part.metadata?.compaction_tail === true)) continue result.push({ start: i, end: messages.length, diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 45af487822..4e16a33540 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -1497,6 +1497,44 @@ describe("session.compaction.process", () => { }).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 }) })) }) + itCompaction.instance("summarizes previous synthetic tail on repeated compaction", () => { + const stub = llm() + let captured = "" + stub.push(reply("summary one")) + stub.push(reply("summary two", (input) => (captured = JSON.stringify(input.messages)))) + + return Effect.gen(function* () { + const ssn = yield* SessionNs.Service + const session = yield* ssn.create({}) + yield* createUserMessage(session.id, "older") + yield* createUserMessage(session.id, "previous tail") + yield* createCompactionMarker(session.id) + + let msgs = yield* ssn.messages({ sessionID: session.id }) + let parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false }) + + yield* createUserMessage(session.id, "new tail") + yield* createCompactionMarker(session.id) + + msgs = MessageV2.filterCompacted(MessageV2.stream(session.id)) + parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false }) + + const tails = (yield* ssn.messages({ sessionID: session.id })).filter( + (item) => item.info.role === "user" && item.parts.some((part) => part.type === "text" && part.metadata?.compaction_tail === true), + ) + const latestTail = tails.at(-1) + + expect(captured).toContain("previous tail") + expect(captured).toContain("latest-messages") + expect(JSON.stringify(latestTail?.parts)).toContain("new tail") + expect(JSON.stringify(latestTail?.parts)).not.toContain("previous tail") + }).pipe(withCompaction({ llm: stub.layer, config: cfg({ tail_turns: 1, preserve_recent_tokens: 10_000 }) })) + }) + itCompaction.instance( "ignores previous summaries when sizing the serialized tail", Effect.gen(function* () {