Skip to content

Commit 27a17c0

Browse files
sepo-engopencodeclaude
authored
feat(compaction): clearer summary prompt structure for smaller models (#58)
Hand-merged from anomalyco/opencode@dab2637217 ("fix(compaction): adjust instructions and structure to be more clear to smaller models like dsv4 flash", anomalyco#42045). Their patch didn't cherry-pick cleanly -- it conflicts with TKT-379 (the compaction summary must NOT hide that context was compacted; hiding it measurably drops history_search's call rate from 100% to 20%) and with our own splitPrefix/splitSuffix/ retainedCount extension to select(). Reapplied by hand, taking the genuinely good parts and explicitly not the rest: TAKEN: - New SUMMARY_UPDATE_INSTRUCTIONS block: explicit guidance for combining a <prior-summary> with new <conversation> content (carry forward objectives/constraints/decisions, conversation wins on conflict, move Active->Completed, update Objective/Next Move). - buildPrompt() restructured to wrap the actual conversation content in explicit <conversation> tags up front, before the instructions, instead of spreading context items loose at the end of the prompt array. This is the actual "clearer to smaller models" improvement. - Tag renamed <previous-summary> -> <prior-summary> to match. Fixed every place that tag name is asserted or referenced, including in the SECOND compaction pipeline (packages/opencode/src/session/ compaction.ts) that turns out to share this same buildPrompt() -- and its own agent system prompt (packages/core/src/plugin/agent.ts PROMPT_COMPACTION / packages/opencode/src/agent/prompt/ compaction.txt), which told the model to look for the old tag name. NOT TAKEN, deliberately (Sean: "don't take their side"): - Upstream's version of this same instruction says "Do not mention the summary process or that context was compacted" -- the exact opposite of TKT-379's fix. Kept our version unchanged. Added a test pinning this so a future upstream sync can't silently reintroduce it via a cleaner-looking cherry-pick. - Upstream's select() also drops splitPrefix/splitSuffix (character- level splitting of a message straddling the retention boundary) in favor of whole-message-only slicing. Our fork extended that same logic with retainedCount (feeds SessionEvent.Compaction.Ended.retainedTailMessages) -- left entirely untouched. NOT TOUCHED, flagged to feedback instead (anomalyco#227): PROMPT_COMPACTION's own "do not mention... compacting" sentence (a second, separate instance of the same class of bug TKT-379 fixed) and the packages/core/src/v1/config/config.ts tail_turns docstring clarity change -- both belong to the second compaction pipeline, whose liveness relative to the first is TKT-377's still-open question, not something to resolve as a side effect of this hand-merge. Co-authored-by: opencode <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent f0a4a2a commit 27a17c0

6 files changed

Lines changed: 59 additions & 10 deletions

File tree

‎packages/core/src/plugin/agent.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const PROMPT_COMPACTION = `You are an anchored context summarization assistant f
3434
3535
Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work.
3636
37-
If the prompt includes a <previous-summary> block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts.
37+
If the prompt includes a <prior-summary> block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts.
3838
3939
Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs.
4040

‎packages/core/src/session/compaction.ts‎

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ Rules:
6464
"compaction" case), and that marker must stay -- TKT-379, diary 2610: hiding it drops
6565
history_search's call rate from 100% to 20% because the model receives no cue that older detail
6666
might exist at all.`
67+
const SUMMARY_UPDATE_INSTRUCTIONS = `The <prior-summary> summarizes everything that happened before the <conversation>. Construct a new summary that combines both. The <prior-summary> is discarded after this: anything you do not carry into the new summary is lost.
68+
69+
When combining:
70+
- Carry forward objectives, constraints, user directives, decisions, and parallel workstreams from the <prior-summary> even when the <conversation> does not mention them. Drop only what is finished and no longer needed.
71+
- The <conversation> is more recent than the <prior-summary>. Where they conflict, the conversation wins: state the corrected fact and drop the old claim.
72+
- Add new progress, decisions, constraints, and context from the conversation.
73+
- Move completed work from "Active" to "Completed".
74+
- If a blocker has been resolved, update the summary to reflect that while keeping any details still needed to continue the work.
75+
- Update "Objective" and "Next Move" to reflect the current work state.`
6776

6877
export type Entry = {
6978
readonly seq: number
@@ -250,14 +259,21 @@ const select = (
250259
}
251260
}
252261

253-
export const buildPrompt = (input: { readonly previousSummary?: string; readonly context: readonly string[] }) =>
254-
[
255-
input.previousSummary
256-
? `Update the anchored summary below using the conversation history above.\nPreserve still-true details, remove stale details, and merge in the new facts.\n<previous-summary>\n${input.previousSummary}\n</previous-summary>`
257-
: "Create a new anchored summary from the conversation history.",
262+
export const buildPrompt = (input: { readonly previousSummary?: string; readonly context: readonly string[] }) => {
263+
const conversation = `Here is the conversation so far:\n\n<conversation>\n${input.context.join("\n\n")}\n</conversation>`
264+
if (!input.previousSummary)
265+
return [
266+
conversation,
267+
"Create a new anchored summary from the conversation history in the <conversation> tags above so another coding agent can continue the work.",
268+
SUMMARY_TEMPLATE,
269+
].join("\n\n")
270+
return [
271+
conversation,
272+
`Here is the summary of the conversation before the <conversation> above:\n\n<prior-summary>\n${input.previousSummary}\n</prior-summary>`,
273+
SUMMARY_UPDATE_INSTRUCTIONS,
258274
SUMMARY_TEMPLATE,
259-
...input.context,
260275
].join("\n\n")
276+
}
261277

262278
export const make = (dependencies: Dependencies) => {
263279
const config = settings(dependencies.config)

‎packages/core/test/session-compaction.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,45 @@ function assistant(
3535
test("compaction prompt preserves detailed work state and relevant files", () => {
3636
const prompt = SessionCompaction.buildPrompt({ context: ["conversation history"] })
3737

38+
expect(prompt).toStartWith(
39+
"Here is the conversation so far:\n\n<conversation>\nconversation history\n</conversation>",
40+
)
41+
expect(prompt.indexOf("</conversation>")).toBeLessThan(prompt.indexOf("Create a new anchored summary"))
42+
expect(prompt).toContain("conversation history in the <conversation> tags above")
3843
expect(prompt).toContain("## Work State\n### Completed")
3944
expect(prompt).toContain("### Active")
4045
expect(prompt).toContain("### Blocked")
4146
expect(prompt).toContain("## Relevant Files")
4247
})
4348

49+
test("compaction prompt gives update instructions for a prior summary", () => {
50+
const prompt = SessionCompaction.buildPrompt({
51+
context: ["new conversation"],
52+
previousSummary: "existing summary",
53+
})
54+
55+
expect(prompt.indexOf("<conversation>")).toBeLessThan(prompt.indexOf("<prior-summary>"))
56+
expect(prompt.indexOf("</prior-summary>")).toBeLessThan(prompt.indexOf("The <prior-summary> summarizes"))
57+
expect(prompt).toContain(
58+
"Carry forward objectives, constraints, user directives, decisions, and parallel workstreams from the <prior-summary>",
59+
)
60+
expect(prompt).toContain('Move completed work from "Active" to "Completed".')
61+
expect(prompt).toContain('Update "Objective" and "Next Move" to reflect the current work state.')
62+
})
63+
64+
// TKT-379, diary 2610: hiding that context was compacted drops history_search's call rate from
65+
// 100% to 20% -- the model needs the cue that older detail might exist. Upstream's own version of
66+
// this prompt instructs the OPPOSITE ("do not mention...that context was compacted"); this pins
67+
// that we deliberately do not take that instruction when porting their prompt-clarity
68+
// improvements (anomalyco/opencode@dab2637, "fix(compaction): adjust instructions and structure
69+
// to be more clear to smaller models").
70+
test("compaction prompt still requires the compaction marker to stay visible (TKT-379 -- do not adopt upstream's opposite instruction)", () => {
71+
const prompt = SessionCompaction.buildPrompt({ context: ["conversation history"] })
72+
73+
expect(prompt).toContain("that marker must stay -- TKT-379")
74+
expect(prompt).not.toContain("Do not mention the summary process or that context was compacted.")
75+
})
76+
4477
test("exceedsCapacity applies the estimator safety factor to the estimated portion -- a request the raw estimate clears still exceeds capacity once inflated by 1.2x (TKT-377, diary 2584/2594: char/4 under-counts structured tool output by up to 31%)", () => {
4578
// context=100000, buffer=1000 -> usable capacity is 99000. estimatedTokens=90000 clears that
4679
// raw (90000 <= 99000), but 90000 * 1.2 = 108000 exceeds it -- exactly the gap the live

‎packages/core/test/session-runner.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1143,7 +1143,7 @@ describe("SessionRunnerLLM", () => {
11431143

11441144
expect(requests).toHaveLength(2)
11451145
expect(userTexts(requests[0])[0]).toContain(
1146-
"<previous-summary>\n## Objective\n- Preserve the task\n</previous-summary>",
1146+
"<prior-summary>\n## Objective\n- Preserve the task\n</prior-summary>",
11471147
)
11481148
expect(userTexts(requests[0])[0]).toContain("Recent exact request")
11491149
expect((yield* (yield* SessionStore.Service).context(sessionID))[0]).toMatchObject({

‎packages/opencode/src/agent/prompt/compaction.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ You are an anchored context summarization assistant for coding sessions.
22

33
Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work.
44

5-
If the prompt includes a <previous-summary> block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts.
5+
If the prompt includes a <prior-summary> block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts.
66

77
Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs.
88

‎packages/opencode/test/session/compaction.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1484,7 +1484,7 @@ describe("session.compaction.process", () => {
14841484
expect(parent).toBeTruthy()
14851485
yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false })
14861486

1487-
expect(captured).toContain("<previous-summary>")
1487+
expect(captured).toContain("<prior-summary>")
14881488
expect(captured).toContain("summary one")
14891489
expect(captured.match(/summary one/g)?.length).toBe(1)
14901490
expect(captured).toContain("## Important Details")

0 commit comments

Comments
 (0)