feat(core): compact v2 session context (#30986)

This commit is contained in:
Kit Langton
2026-06-05 13:17:23 -04:00
committed by GitHub
parent b157200e08
commit 80da87da0e
26 changed files with 571 additions and 298 deletions
@@ -23,13 +23,6 @@ function ownedAssistant(messages: SessionMessage[], messageID: string) {
return message?.type === "assistant" ? message : undefined
}
function activeCompaction(messages: SessionMessage[]) {
const index = messages.findIndex((message) => message.type === "compaction")
if (index < 0) return
const compaction = messages[index]
return compaction?.type === "compaction" ? compaction : undefined
}
function activeShell(messages: SessionMessage[], callID: string) {
const index = messages.findIndex((message) => message.type === "shell" && message.callID === callID)
if (index < 0) return
@@ -410,32 +403,21 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
})
break
case "session.next.retried":
break
case "session.next.compaction.started":
case "session.next.compaction.delta":
break
case "session.next.compaction.ended":
update(event.properties.sessionID, (draft) => {
prepend(draft, {
id: event.properties.messageID,
type: "compaction",
reason: event.properties.reason,
summary: "",
summary: event.properties.text,
recent: event.properties.recent,
time: { created: event.properties.timestamp },
})
})
break
case "session.next.compaction.delta":
update(event.properties.sessionID, (draft) => {
const match = activeCompaction(draft)
if (match) match.summary += event.properties.text
})
break
case "session.next.compaction.ended":
update(event.properties.sessionID, (draft) => {
const match = activeCompaction(draft)
if (!match) return
match.summary = event.properties.text
match.include = event.properties.include
})
break
}
}
@@ -231,7 +231,7 @@ function ShellMessage(props: { message: SessionMessageShell }) {
}
function CompactionMessage(props: { message: SessionMessageCompaction }) {
const { theme, syntax } = useTheme()
const { theme } = useTheme()
return (
<box
marginTop={1}
@@ -240,23 +240,7 @@ function CompactionMessage(props: { message: SessionMessageCompaction }) {
titleAlignment="center"
borderColor={theme.borderActive}
flexShrink={0}
>
<Show when={props.message.summary}>
{(summary) => (
<box paddingLeft={3} paddingTop={1}>
<code
filetype="markdown"
drawUnstyledText={false}
streaming={false}
syntaxStyle={syntax()}
content={summary().trim()}
conceal={true}
fg={theme.text}
/>
</box>
)}
</Show>
</box>
/>
)
}
+23 -56
View File
@@ -24,6 +24,7 @@ import { SessionMessage } from "@opencode-ai/core/session/message"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { ModelV2 } from "@opencode-ai/core/model"
import { EventV2 } from "@opencode-ai/core/event"
import { buildPrompt } from "@opencode-ai/core/session/compaction"
const log = Log.create({ service: "session.compaction" })
@@ -43,42 +44,6 @@ const PRUNE_PROTECTED_TOOLS = ["skill"]
const DEFAULT_TAIL_TURNS = 2
const MIN_PRESERVE_RECENT_TOKENS = 2_000
const MAX_PRESERVE_RECENT_TOKENS = 8_000
const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <template> and keep the section order unchanged. Do not include the <template> tags in your response.
<template>
## Goal
- [single-sentence task summary]
## Constraints & Preferences
- [user constraints, preferences, specs, or "(none)"]
## Progress
### Done
- [completed work or "(none)"]
### In Progress
- [current work or "(none)"]
### Blocked
- [blockers or "(none)"]
## Key Decisions
- [decision and why, or "(none)"]
## Next Steps
- [ordered next actions or "(none)"]
## Critical Context
- [important technical facts, errors, open questions, or "(none)"]
## Relevant Files
- [file or directory path: why it matters, or "(none)"]
</template>
Rules:
- Keep every section, even when empty.
- Use terse bullets, not prose paragraphs.
- Preserve exact file paths, commands, error strings, and identifiers when known.
- Do not mention the summary process or that context was compacted.`
type Turn = {
start: number
end: number
@@ -124,19 +89,6 @@ function completedCompactions(messages: SessionV1.WithParts[]) {
})
}
function buildPrompt(input: { previousSummary?: string; context: string[] }) {
const anchor = input.previousSummary
? [
"Update the anchored summary below using the conversation history above.",
"Preserve still-true details, remove stale details, and merge in the new facts.",
"<previous-summary>",
input.previousSummary,
"</previous-summary>",
].join("\n")
: "Create a new anchored summary from the conversation history above."
return [anchor, SUMMARY_TEMPLATE, ...input.context].join("\n\n")
}
function preserveRecentBudget(input: { cfg: ConfigV1.Info; model: Provider.Model }) {
return (
input.cfg.compaction?.preserve_recent_tokens ??
@@ -410,6 +362,18 @@ export const layer = Layer.effect(
stripMedia: true,
toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
})
const tailIndex = selected.tail_start_id
? history.findIndex((message) => message.info.id === selected.tail_start_id)
: -1
const recent =
tailIndex < 0
? ""
: JSON.stringify(
yield* MessageV2.toModelMessagesEffect(history.slice(tailIndex), model, {
stripMedia: true,
toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
}),
)
const ctx = yield* InstanceState.context
const msg: SessionV1.Assistant = {
id: MessageID.ascending(),
@@ -572,12 +536,15 @@ export const layer = Layer.effect(
},
)
if (flags.experimentalEventSystem) {
yield* events.publish(SessionEvent.Compaction.Ended, {
sessionID: input.sessionID,
timestamp: DateTime.makeUnsafe(Date.now()),
text: summary ?? "",
include: selected.tail_start_id,
})
if (summary)
yield* events.publish(SessionEvent.Compaction.Ended, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.make(input.parentID),
timestamp: DateTime.makeUnsafe(Date.now()),
reason: input.auto ? "auto" : "manual",
text: summary ?? "",
recent,
})
}
yield* events.publish(Event.Compacted, { sessionID: input.sessionID })
}
@@ -610,7 +577,7 @@ export const layer = Layer.effect(
if (flags.experimentalEventSystem) {
yield* events.publish(SessionEvent.Compaction.Started, {
sessionID: input.sessionID,
messageID: SessionMessage.ID.create(),
messageID: SessionMessage.ID.make(msg.id),
timestamp: DateTime.makeUnsafe(Date.now()),
reason: input.auto ? "auto" : "manual",
})
+1 -7
View File
@@ -1,7 +1 @@
const CHARS_PER_TOKEN = 4
export function estimate(input: string) {
return Math.max(0, Math.round((input || "").length / CHARS_PER_TOKEN))
}
export * as Token from "./token"
export { Token, estimate } from "@opencode-ai/core/util/token"