Skip to content

Commit 8131d98

Browse files
committed
core: show compaction summary in TUI to explain what context was retained
When context window overflows, users now see a summary of what was retained instead of just a generic compaction notice. This helps users understand why certain parts of the conversation may no longer be immediately accessible and what key information was preserved.
1 parent 203d675 commit 8131d98

11 files changed

Lines changed: 345 additions & 77 deletions

File tree

packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ function activeAssistant(messages: SessionMessage[]) {
1717
return assistant?.type === "assistant" ? assistant : undefined
1818
}
1919

20+
function activeCompaction(messages: SessionMessage[]) {
21+
const index = messages.findLastIndex((message) => message.type === "compaction")
22+
if (index < 0) return
23+
const compaction = messages[index]
24+
return compaction?.type === "compaction" ? compaction : undefined
25+
}
26+
2027
function latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
2128
return assistant?.content.findLast(
2229
(item): item is SessionMessageAssistantTool => item.type === "tool" && (callID === undefined || item.id === callID),
@@ -214,18 +221,31 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
214221
break
215222
case "session.next.retried":
216223
break
217-
case "session.next.compacted":
224+
case "session.next.compaction.started":
218225
update(event.properties.sessionID, (draft) => {
219226
draft.push({
220227
id: event.properties.id,
221228
type: "compaction",
222-
sessionID: event.properties.sessionID,
223-
auto: event.properties.auto,
224-
overflow: event.properties.overflow,
229+
reason: event.properties.reason,
230+
summary: "",
225231
time: { created: event.properties.timestamp },
226232
})
227233
})
228234
break
235+
case "session.next.compaction.delta":
236+
update(event.properties.sessionID, (draft) => {
237+
const match = activeCompaction(draft)
238+
if (match) match.summary += event.properties.text
239+
})
240+
break
241+
case "session.next.compaction.ended":
242+
update(event.properties.sessionID, (draft) => {
243+
const match = activeCompaction(draft)
244+
if (!match) return
245+
match.summary = event.properties.text
246+
match.include = event.properties.include
247+
})
248+
break
229249
}
230250
})
231251

packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,13 +200,13 @@ function CompactionMessage(props: { message: SessionMessageCompaction }) {
200200
<box
201201
marginTop={1}
202202
border={["top"]}
203-
title={props.message.auto ? " Auto Compaction " : " Compaction "}
203+
title={props.message.reason === "auto" ? " Auto Compaction " : " Compaction "}
204204
titleAlignment="center"
205205
borderColor={theme.borderActive}
206206
flexShrink={0}
207207
>
208-
<Show when={props.message.overflow}>
209-
<text fg={theme.textMuted}>Context overflow triggered this compaction.</text>
208+
<Show when={props.message.summary}>
209+
<text fg={theme.textMuted}>{props.message.summary}</text>
210210
</Show>
211211
</box>
212212
)

packages/opencode/src/session/compaction.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ import { Config } from "@/config/config"
1414
import { NotFoundError } from "@/storage/storage"
1515
import { ModelID, ProviderID } from "@/provider/schema"
1616
import { Effect, Layer, Context, Schema } from "effect"
17+
import * as DateTime from "effect/DateTime"
1718
import { InstanceState } from "@/effect/instance-state"
1819
import { isOverflow as overflow, usable } from "./overflow"
1920
import { makeRuntime } from "@/effect/run-service"
2021
import { fn } from "@/util/fn"
22+
import { SyncEvent } from "@/sync"
23+
import { SessionEvent } from "@/v2/session-event"
2124

2225
const log = Log.create({ service: "session.compaction" })
2326

@@ -556,7 +559,21 @@ export const layer: Layer.Layer<
556559
}
557560

558561
if (processor.message.error) return "stop"
559-
if (result === "continue") yield* bus.publish(Event.Compacted, { sessionID: input.sessionID })
562+
if (result === "continue") {
563+
const summary = summaryText(
564+
(yield* session.messages({ sessionID: input.sessionID })).find((item) => item.info.id === msg.id) ?? {
565+
info: msg,
566+
parts: [],
567+
},
568+
)
569+
SyncEvent.run(SessionEvent.Compaction.Ended.Sync, {
570+
sessionID: input.sessionID,
571+
timestamp: DateTime.makeUnsafe(Date.now()),
572+
text: summary ?? "",
573+
include: selected.tail_start_id,
574+
})
575+
yield* bus.publish(Event.Compacted, { sessionID: input.sessionID })
576+
}
560577
return result
561578
})
562579

@@ -583,6 +600,12 @@ export const layer: Layer.Layer<
583600
auto: input.auto,
584601
overflow: input.overflow,
585602
})
603+
SyncEvent.run(SessionEvent.Compaction.Started.Sync, {
604+
id: SessionEvent.ID.create(),
605+
sessionID: input.sessionID,
606+
timestamp: DateTime.makeUnsafe(Date.now()),
607+
reason: input.auto ? "auto" : "manual",
608+
})
586609
})
587610

588611
return Service.of({

packages/opencode/src/session/processor.ts

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -433,19 +433,21 @@ export const layer: Layer.Layer<
433433

434434
case "start-step":
435435
if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track()
436-
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
437-
SyncEvent.run(SessionEvent.Step.Started.Sync, {
438-
id: SessionEvent.ID.create(),
439-
sessionID: ctx.sessionID,
440-
agent: input.assistantMessage.agent,
441-
model: {
442-
id: ctx.model.id,
443-
providerID: ctx.model.providerID,
444-
variant: input.assistantMessage.variant,
445-
},
446-
snapshot: ctx.snapshot,
447-
timestamp: DateTime.makeUnsafe(Date.now()),
448-
})
436+
if (!ctx.assistantMessage.summary) {
437+
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
438+
SyncEvent.run(SessionEvent.Step.Started.Sync, {
439+
id: SessionEvent.ID.create(),
440+
sessionID: ctx.sessionID,
441+
agent: input.assistantMessage.agent,
442+
model: {
443+
id: ctx.model.id,
444+
providerID: ctx.model.providerID,
445+
variant: input.assistantMessage.variant,
446+
},
447+
snapshot: ctx.snapshot,
448+
timestamp: DateTime.makeUnsafe(Date.now()),
449+
})
450+
}
449451
yield* session.updatePart({
450452
id: PartID.ascending(),
451453
messageID: ctx.assistantMessage.id,
@@ -462,15 +464,17 @@ export const layer: Layer.Layer<
462464
usage: value.usage,
463465
metadata: value.providerMetadata,
464466
})
465-
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
466-
SyncEvent.run(SessionEvent.Step.Ended.Sync, {
467-
sessionID: ctx.sessionID,
468-
finish: value.finishReason,
469-
cost: usage.cost,
470-
tokens: usage.tokens,
471-
snapshot: completedSnapshot,
472-
timestamp: DateTime.makeUnsafe(Date.now()),
473-
})
467+
if (!ctx.assistantMessage.summary) {
468+
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
469+
SyncEvent.run(SessionEvent.Step.Ended.Sync, {
470+
sessionID: ctx.sessionID,
471+
finish: value.finishReason,
472+
cost: usage.cost,
473+
tokens: usage.tokens,
474+
snapshot: completedSnapshot,
475+
timestamp: DateTime.makeUnsafe(Date.now()),
476+
})
477+
}
474478
ctx.assistantMessage.finish = value.finishReason
475479
ctx.assistantMessage.cost += usage.cost
476480
ctx.assistantMessage.tokens = usage.tokens
@@ -515,11 +519,13 @@ export const layer: Layer.Layer<
515519
}
516520

517521
case "text-start":
518-
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
519-
SyncEvent.run(SessionEvent.Text.Started.Sync, {
520-
sessionID: ctx.sessionID,
521-
timestamp: DateTime.makeUnsafe(Date.now()),
522-
})
522+
if (!ctx.assistantMessage.summary) {
523+
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
524+
SyncEvent.run(SessionEvent.Text.Started.Sync, {
525+
sessionID: ctx.sessionID,
526+
timestamp: DateTime.makeUnsafe(Date.now()),
527+
})
528+
}
523529
ctx.currentText = {
524530
id: PartID.ascending(),
525531
messageID: ctx.assistantMessage.id,
@@ -534,6 +540,13 @@ export const layer: Layer.Layer<
534540

535541
case "text-delta":
536542
if (!ctx.currentText) return
543+
if (ctx.assistantMessage.summary) {
544+
SyncEvent.run(SessionEvent.Compaction.Delta.Sync, {
545+
sessionID: ctx.sessionID,
546+
text: value.text,
547+
timestamp: DateTime.makeUnsafe(Date.now()),
548+
})
549+
}
537550
ctx.currentText.text += value.text
538551
if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata
539552
yield* session.updatePartDelta({
@@ -558,12 +571,14 @@ export const layer: Layer.Layer<
558571
},
559572
{ text: ctx.currentText.text },
560573
)).text
561-
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
562-
SyncEvent.run(SessionEvent.Text.Ended.Sync, {
563-
sessionID: ctx.sessionID,
564-
text: ctx.currentText.text,
565-
timestamp: DateTime.makeUnsafe(Date.now()),
566-
})
574+
if (!ctx.assistantMessage.summary) {
575+
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
576+
SyncEvent.run(SessionEvent.Text.Ended.Sync, {
577+
sessionID: ctx.sessionID,
578+
text: ctx.currentText.text,
579+
timestamp: DateTime.makeUnsafe(Date.now()),
580+
})
581+
}
567582
{
568583
const end = Date.now()
569584
ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end }

packages/opencode/src/session/projectors-next.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ function sqlite(db: Database.TxOrDb, sessionID: SessionID): SessionMessageUpdate
3737
.map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type }))
3838
.find((message): message is SessionMessage.Assistant => message.type === "assistant" && !message.time.completed)
3939
},
40+
getCurrentCompaction() {
41+
return db
42+
.select()
43+
.from(SessionMessageTable)
44+
.where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction")))
45+
.orderBy(desc(SessionMessageTable.id))
46+
.all()
47+
.map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type }))
48+
.find((message): message is SessionMessage.Compaction => message.type === "compaction")
49+
},
4050
updateAssistant(assistant) {
4151
const { id, type, ...data } = assistant
4252
db.update(SessionMessageTable)
@@ -50,6 +60,19 @@ function sqlite(db: Database.TxOrDb, sessionID: SessionID): SessionMessageUpdate
5060
)
5161
.run()
5262
},
63+
updateCompaction(compaction) {
64+
const { id, type, ...data } = compaction
65+
db.update(SessionMessageTable)
66+
.set({ data: encodeMessageData(data) })
67+
.where(
68+
and(
69+
eq(SessionMessageTable.id, id),
70+
eq(SessionMessageTable.session_id, sessionID),
71+
eq(SessionMessageTable.type, type),
72+
),
73+
)
74+
.run()
75+
},
5376
appendMessage(message) {
5477
const { id, type, ...data } = message
5578
db.insert(SessionMessageTable)
@@ -118,7 +141,13 @@ export default [
118141
SyncEvent.project(SessionEvent.Retried.Sync, (db, data) => {
119142
update(db, { type: "session.next.retried", data })
120143
}),
121-
SyncEvent.project(SessionEvent.Compacted.Sync, (db, data) => {
122-
update(db, { type: "session.next.compacted", data })
144+
SyncEvent.project(SessionEvent.Compaction.Started.Sync, (db, data) => {
145+
update(db, { type: "session.next.compaction.started", data })
146+
}),
147+
SyncEvent.project(SessionEvent.Compaction.Delta.Sync, (db, data) => {
148+
update(db, { type: "session.next.compaction.delta", data })
149+
}),
150+
SyncEvent.project(SessionEvent.Compaction.Ended.Sync, (db, data) => {
151+
update(db, { type: "session.next.compaction.ended", data })
123152
}),
124153
]

packages/opencode/src/v2/session-event.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -272,17 +272,38 @@ export const Retried = Event.define({
272272
})
273273
export type Retried = Schema.Schema.Type<typeof Retried>
274274

275-
export const Compacted = Event.define({
276-
type: "session.next.compacted",
277-
aggregate: "sessionID",
278-
schema: {
279-
...Base,
280-
id: ID,
281-
auto: Schema.Boolean,
282-
overflow: Schema.Boolean.pipe(Schema.optional),
283-
},
284-
})
285-
export type Compacted = Schema.Schema.Type<typeof Compacted>
275+
export namespace Compaction {
276+
export const Started = Event.define({
277+
type: "session.next.compaction.started",
278+
aggregate: "sessionID",
279+
schema: {
280+
...Base,
281+
id: ID,
282+
reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
283+
},
284+
})
285+
export type Started = Schema.Schema.Type<typeof Started>
286+
287+
export const Delta = Event.define({
288+
type: "session.next.compaction.delta",
289+
aggregate: "sessionID",
290+
schema: {
291+
...Base,
292+
text: Schema.String,
293+
},
294+
})
295+
296+
export const Ended = Event.define({
297+
type: "session.next.compaction.ended",
298+
aggregate: "sessionID",
299+
schema: {
300+
...Base,
301+
text: Schema.String,
302+
include: Schema.String.pipe(Schema.optional),
303+
},
304+
})
305+
export type Ended = Schema.Schema.Type<typeof Ended>
306+
}
286307

287308
export const All = Schema.Union(
288309
[
@@ -304,13 +325,24 @@ export const All = Schema.Union(
304325
Reasoning.Delta,
305326
Reasoning.Ended,
306327
Retried,
307-
Compacted,
328+
Compaction.Started,
329+
Compaction.Delta,
330+
Compaction.Ended,
308331
],
309332
{
310333
mode: "oneOf",
311334
},
312335
).pipe(Schema.toTaggedUnion("type"))
313336

337+
// user
338+
// assistant
339+
// assistant
340+
// assistant
341+
// user
342+
// compaction marker
343+
// -> text
344+
// assistant
345+
314346
export type Event = Schema.Schema.Type<typeof All>
315347
export type Type = Event["type"]
316348

0 commit comments

Comments
 (0)