Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions packages/opencode/src/session/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a later user message contains only whitespace, currentRequest treats it as the in-flight request and omits the actual request from the continuation prompt. Require a non-whitespace text part in the findLast predicate so the helper matches the stated substantive-request behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/session/compaction.ts, line 59:

<comment>When a later user message contains only whitespace, `currentRequest` treats it as the in-flight request and omits the actual request from the continuation prompt. Require a non-whitespace text part in the `findLast` predicate so the helper matches the stated substantive-request behavior.</comment>

<file context>
@@ -49,6 +49,22 @@ type CompletedCompaction = {
+
+// 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
</file context>
Suggested change
const request = messages.findLast((m) => m.info.role === "user" && m.parts.some(isRequestText))
const request = messages.findLast((m) => m.info.role === "user" && m.parts.some((part) => isRequestText(part) && part.text.trim()))

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")
Expand Down Expand Up @@ -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<user_request>\n${request}\n</user_request>`
: "",
"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,
Expand Down
22 changes: 20 additions & 2 deletions packages/opencode/test/session/compaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("<user_request>\nhello\n</user_request>")
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("<user_request>\nhello\n</user_request>")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new test "quotes the original request, not a prior continue prompt, on a second compaction" does not actually verify its stated intent. In the buggy scenario it guards against (currentRequest picking up the prior synthetic nudge as the request), the second nudge would wrap the first nudge's text, which itself already contains the <user_request>\nhello\n</user_request> block. So toContain("<user_request>\nhello\n</user_request>") passes both with and without the !part.synthetic filter, and the test never fails on the regression it is named for. Assert the directive appears exactly once (or that the quoted request block contains only "hello"), e.g. expect the text to be the exact expected string, or check text.split("Continue the work from Next Move") has length 2 rather than a single toContain.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/test/session/compaction.test.ts, line 943:

<comment>The new test "quotes the original request, not a prior continue prompt, on a second compaction" does not actually verify its stated intent. In the buggy scenario it guards against (currentRequest picking up the prior synthetic nudge as the request), the second nudge would wrap the first nudge's text, which itself already contains the `<user_request>\nhello\n</user_request>` block. So `toContain("<user_request>\nhello\n</user_request>")` passes both with and without the `!part.synthetic` filter, and the test never fails on the regression it is named for. Assert the directive appears exactly once (or that the quoted request block contains only "hello"), e.g. expect the text to be the exact expected string, or check `text.split("Continue the work from Next Move")` has length 2 rather than a single `toContain`.</comment>

<file context>
@@ -921,11 +921,29 @@ describe("session.compaction.process", () => {
+      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("<user_request>\nhello\n</user_request>")
+    }),
+  )
</file context>
Suggested change
expect(last?.parts[0]?.type === "text" && last.parts[0].text).toContain("<user_request>\nhello\n</user_request>")
const text = last?.parts[0]?.type === "text" ? last.parts[0].text : ""
expect(text).toContain("<user_request>\nhello\n</user_request>")
// The directive must appear exactly once: a nested prior nudge would quote it again.
expect(text.split("Continue the work from Next Move")).toHaveLength(2)

}),
)

itCompaction.instance(
"persists tail_start_id for retained recent turns",
Effect.gen(function* () {
Expand Down Expand Up @@ -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)
Expand Down
Loading