From 44cd78acad7db516b287ebe2789d087fe13fbb08 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 10 Sep 2026 16:30:26 -0700 Subject: [PATCH] fix(opencode): carry the in-flight request through auto-compaction Scheduled compaction can start the retained tail inside the current turn, so the user's own instruction is summarized into a one-line Objective and the model resumes from a paraphrase plus a nudge that permits stopping. In production it replied "Here's where things stand..." and asked what the request meant. The continue prompt now quotes the request verbatim and tells the model to resume without restating the summary. USE-2680. Co-Authored-By: Claude Fable 5.1 --- packages/opencode/src/session/compaction.ts | 33 ++++++++++++++++--- .../opencode/test/session/compaction.test.ts | 22 +++++++++++-- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 4aa1ccdec65d..e2be3d4a88d1 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -49,6 +49,22 @@ type CompletedCompaction = { summary: string | undefined } +const REQUEST_MAX_CHARS = 8_000 + +const isRequestText = (part: SessionV1.Part): part is SessionV1.TextPart => + part.type === "text" && !part.synthetic && !part.ignored + +// The verbatim request the turn is carrying out; the summary only paraphrases it. +function currentRequest(messages: SessionV1.WithParts[]) { + const request = messages.findLast((m) => m.info.role === "user" && m.parts.some(isRequestText)) + const text = + request?.parts + .filter(isRequestText) + .map((part) => part.text) + .join("\n") ?? "" + return text.length <= REQUEST_MAX_CHARS ? text : `${text.slice(0, REQUEST_MAX_CHARS)}\n[truncated]` +} + function summaryText(message: SessionV1.WithParts) { const text = message.parts .filter((part): part is SessionV1.TextPart => part.type === "text") @@ -480,11 +496,18 @@ const layer = Layer.effect( agent: userMessage.agent, model: userMessage.model, }) - const text = - (input.overflow - ? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files.\n\n" - : "") + - "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed." + const request = currentRequest(input.messages) + const text = [ + input.overflow + ? "The previous request exceeded the provider's size limit due to large media attachments. The conversation was compacted and media files were removed from context. If the user was asking about attached images or files, explain that the attachments were too large to process and suggest they try again with smaller or fewer files." + : "", + request + ? `The request you were carrying out when the conversation was compacted:\n\n${request}\n` + : "", + "Continue the work from Next Move; if nothing remains, say so briefly. Do not restate the summary. Only stop to ask the user if you cannot proceed without their input.", + ] + .filter(Boolean) + .join("\n\n") yield* session.updatePart({ id: PartID.ascending(), messageID: continueMsg.id, diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index b1ce8e55d9ca..0b8652c7ed97 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -921,11 +921,29 @@ describe("session.compaction.process", () => { metadata: { compaction_continue: true }, }) if (last?.parts[0]?.type === "text") { - expect(last.parts[0].text).toContain("Continue if you have next steps") + expect(last.parts[0].text).toContain("\nhello\n") + expect(last.parts[0].text).toContain("Continue the work from Next Move") } }), ) + it.instance( + "quotes the original request, not a prior continue prompt, on a second compaction", + Effect.gen(function* () { + const ssn = yield* SessionNs.Service + const session = yield* ssn.create({}) + const msg = yield* createUserMessage(session.id, "hello") + const first = yield* ssn.messages({ sessionID: session.id }) + yield* SessionCompaction.use.process({ parentID: msg.id, messages: first, sessionID: session.id, auto: true }) + + const second = yield* ssn.messages({ sessionID: session.id }) + yield* SessionCompaction.use.process({ parentID: msg.id, messages: second, sessionID: session.id, auto: true }) + + const last = (yield* ssn.messages({ sessionID: session.id })).at(-1) + expect(last?.parts[0]?.type === "text" && last.parts[0].text).toContain("\nhello\n") + }), + ) + itCompaction.instance( "persists tail_start_id for retained recent turns", Effect.gen(function* () { @@ -1118,7 +1136,7 @@ describe("session.compaction.process", () => { (msg) => msg.info.role === "user" && msg.parts.some( - (part) => part.type === "text" && part.synthetic && part.text.includes("Continue if you have next steps"), + (part) => part.type === "text" && part.synthetic && part.metadata?.compaction_continue === true, ), ), ).toBe(false)