From e07f3b3a1e00c48886668fe98469ba10e242980c Mon Sep 17 00:00:00 2001 From: adao Date: Fri, 15 May 2026 19:27:42 +0800 Subject: [PATCH 1/2] fix(session): compact finished overflowed turns --- packages/opencode/src/session/prompt.ts | 22 ++++--- packages/opencode/test/session/prompt.test.ts | 59 +++++++++++++++++-- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index ba9a4d6f1a0..90922fe708e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1668,6 +1668,19 @@ NOTE: At any point in time through this workflow you should feel free to ask the const hasToolCalls = lastAssistantMsg?.parts.some((part) => part.type === "tool" && !part.metadata?.providerExecuted) ?? false + if (lastFinished && lastFinished.summary !== true) { + const modelForCompaction = yield* getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID).pipe( + Effect.exit, + ) + if ( + Exit.isSuccess(modelForCompaction) && + (yield* compaction.isOverflow({ tokens: lastFinished.tokens, model: modelForCompaction.value })) + ) { + yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true }) + continue + } + } + if ( lastAssistant?.finish && !["tool-calls"].includes(lastAssistant.finish) && @@ -1707,15 +1720,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the continue } - if ( - lastFinished && - lastFinished.summary !== true && - (yield* compaction.isOverflow({ tokens: lastFinished.tokens, model })) - ) { - yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true }) - continue - } - const agent = yield* agents.get(lastUser.agent) if (!agent) { const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 891efc18721..9eba467149a 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -163,7 +163,22 @@ const blockingProcessor = Layer.succeed( }), ) -function makeHttp(input?: { processor?: "blocking" }) { +let fakeCompactionCreateCount = 0 +const fakeOverflowCompaction = Layer.succeed( + SessionCompaction.Service, + SessionCompaction.Service.of({ + isOverflow: () => Effect.succeed(true), + prune: () => Effect.void, + process: () => Effect.succeed("stop" as const), + create: () => + Effect.sync(() => { + fakeCompactionCreateCount++ + throw new Error("fake compaction create called") + }), + }), +) + +function makeHttp(input?: { processor?: "blocking"; compaction?: Layer.Layer }) { const deps = Layer.mergeAll( Session.defaultLayer, Snapshot.defaultLayer, @@ -208,11 +223,13 @@ function makeHttp(input?: { processor?: "blocking" }) { Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })), Layer.provideMerge(deps), ) - const compact = SessionCompaction.layer.pipe( - Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })), - Layer.provideMerge(proc), - Layer.provideMerge(deps), - ) + const compact = + input?.compaction ?? + SessionCompaction.layer.pipe( + Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })), + Layer.provideMerge(proc), + Layer.provideMerge(deps), + ) return Layer.mergeAll( TestLLMServer.layer, SessionPrompt.layer.pipe( @@ -235,6 +252,7 @@ function makeHttp(input?: { processor?: "blocking" }) { const it = testEffect(makeHttp()) const race = testEffect(makeHttp({ processor: "blocking" })) +const overflow = testEffect(makeHttp({ compaction: fakeOverflowCompaction })) const unix = process.platform !== "win32" ? it.instance : it.instance.skip // Config that registers a custom "test" provider with a "test-model" model @@ -477,6 +495,35 @@ it.instance( { git: true }, ) +overflow.instance( + "loop creates compaction before exiting when the last finished assistant overflowed", + () => + Effect.gen(function* () { + fakeCompactionCreateCount = 0 + yield* useServerConfig(providerCfg) + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ title: "Overflow finished" }) + const { assistant } = yield* seed(chat.id, { finish: "stop" }) + + yield* sessions.updateMessage({ + ...assistant, + tokens: { + total: 120_000, + input: 120_000, + output: 0, + reasoning: 0, + cache: { read: 0, write: 0 }, + }, + }) + + const exit = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.exit) + expect(Exit.isFailure(exit)).toBe(true) + expect(fakeCompactionCreateCount).toBe(1) + }), + { git: true }, +) + it.instance( "prompt emits v2 prompted and synthetic events", () => From 48f3e3b8152a15f6e803efe1b8ff17fac09fa37c Mon Sep 17 00:00:00 2001 From: adao Date: Fri, 15 May 2026 20:01:30 +0800 Subject: [PATCH 2/2] fix(session): avoid continuing after finished compaction --- packages/opencode/src/session/compaction.ts | 10 ++++++ .../opencode/test/session/compaction.test.ts | 34 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index bc3327c07d4..3ebeed089e0 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -506,6 +506,16 @@ export const layer = Layer.effect( } if (!replay) { + const lastNonSummaryAssistant = [...input.messages] + .reverse() + .find((m) => m.info.role === "assistant" && !m.info.summary)?.info as MessageV2.Assistant | undefined + const shouldAutoContinue = + !lastNonSummaryAssistant?.finish || + lastNonSummaryAssistant.finish === "tool-calls" || + lastNonSummaryAssistant.finish === "unknown" + + if (!shouldAutoContinue) return result + const info = yield* provider.getProvider(userMessage.model.providerID) if ( (yield* plugin.trigger( diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 2bc9b196216..1456afee4d7 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -932,6 +932,40 @@ describe("session.compaction.process", () => { }), ) + itCompaction.instance( + "does not add synthetic continue prompt when auto compacting after a finished response", + Effect.gen(function* () { + const test = yield* TestInstance + const ssn = yield* SessionNs.Service + const session = yield* ssn.create({}) + const msg = yield* createUserMessage(session.id, "hello") + yield* createAssistantMessage(session.id, msg.id, test.directory) + yield* createSummaryCompaction(session.id) + const msgs = yield* ssn.messages({ sessionID: session.id }) + const parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + + const result = yield* SessionCompaction.use.process({ + parentID: parent!, + messages: msgs, + sessionID: session.id, + auto: true, + }) + + const all = yield* ssn.messages({ sessionID: session.id }) + expect(result).toBe("continue") + expect( + all.some( + (msg) => + msg.info.role === "user" && + msg.parts.some( + (part) => part.type === "text" && part.synthetic && part.text.includes("Continue if you have next steps"), + ), + ), + ).toBe(false) + }).pipe(withCompaction()), + ) + itCompaction.instance( "persists tail_start_id for retained recent turns", Effect.gen(function* () {