fix(tui): restore compaction model marker

This commit is contained in:
Kit Langton
2026-07-14 11:35:12 -04:00
parent 634386fe0f
commit 76f205d260
13 changed files with 105 additions and 19 deletions
+13 -12
View File
@@ -117,17 +117,6 @@ export type SessionMessageCompactionRunning = {
recent: string
}
export type SessionMessageCompactionCompleted = {
type: "compaction"
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
status: "completed"
reason: "auto" | "manual"
summary: string
recent: string
}
export type InstructionEntryKey = string
export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } }
@@ -507,6 +496,18 @@ export type SessionMessageModelSelected = {
previous?: ModelRef
}
export type SessionMessageCompactionCompleted = {
type: "compaction"
id: string
metadata?: { [x: string]: JsonValue }
time: { created: number }
status: "completed"
reason: "auto" | "manual"
model?: ModelRef
summary: string
recent: string
}
export type CommandInfo = {
name: string
template: string
@@ -749,7 +750,7 @@ export type SessionCompactionEnded = {
type: "session.compaction.ended"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; reason: "auto" | "manual"; text: string; recent: string }
data: { sessionID: string; reason: "auto" | "manual"; model?: ModelRef; text: string; recent: string }
}
export type SessionRevertCleared = {
+5
View File
@@ -69,6 +69,7 @@ export type AutoInput = {
readonly sessionID: SessionSchema.ID
readonly messages: readonly SessionMessage.Info[]
readonly model: Model
readonly ref: SessionMessage.Assistant["model"]
}
export type ManualInput = {
@@ -80,6 +81,7 @@ export type ManualInput = {
type Plan = {
readonly sessionID: SessionSchema.ID
readonly model: Model
readonly ref: SessionMessage.Assistant["model"]
readonly reason: SessionMessage.Compaction["reason"]
readonly prompt: string
readonly recent: string
@@ -291,6 +293,7 @@ const make = (dependencies: Dependencies) => {
yield* dependencies.events.publish(SessionEvent.Compaction.Ended, {
sessionID: plan.sessionID,
reason: plan.reason,
model: plan.ref,
text: summary,
recent: plan.recent,
})
@@ -302,6 +305,7 @@ const make = (dependencies: Dependencies) => {
return yield* execute({
sessionID: input.sessionID,
model: input.model,
ref: input.ref,
reason: "auto",
...content,
})
@@ -350,6 +354,7 @@ const make = (dependencies: Dependencies) => {
return yield* execute({
sessionID: input.session.id,
model: resolved.model,
ref: resolved.ref,
reason: "manual",
inputID: input.inputID,
...content,
@@ -470,6 +470,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
...current,
status: "completed",
reason: event.data.reason,
model: event.data.model,
summary: event.data.text,
recent: event.data.recent,
})
@@ -482,6 +483,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
status: "completed",
metadata: event.metadata,
reason: event.data.reason,
model: event.data.model,
summary: event.data.text,
recent: event.data.recent,
time: { created: event.created },
+3 -3
View File
@@ -187,7 +187,7 @@ const layer = Layer.effect(
const providerMetadataKey = model.route.providerMetadataKey ?? model.provider
const history = yield* SessionHistory.entriesForRunner(db, session.id, instructions)
const context = history.entries.map((entry) => entry.message)
const compactionInput = { sessionID: session.id, messages: context, model }
const compactionInput = { sessionID: session.id, messages: context, model, ref: resolved.ref }
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
const compacted = yield* compaction.compact(compactionInput)
if (compacted.status === "completed") return { _tag: "RestartAfterCompaction", step: currentStep } as const
@@ -331,8 +331,8 @@ const layer = Layer.effect(
recoverOverflow &&
!publisher.hasRetryEvidence() &&
isContextOverflowFailure(overflowFailure ?? streamFailure) &&
(yield* restore(recoverOverflow({ sessionID: session.id, messages: context, model }))).status ===
"completed"
(yield* restore(recoverOverflow({ sessionID: session.id, messages: context, model, ref: resolved.ref })))
.status === "completed"
)
return { _tag: "RestartAfterOverflowCompaction", step: currentStep } as const
@@ -154,7 +154,13 @@ it.effect("manual compaction summarizes short context instead of no-op", () =>
expect(requests[0]?.generation).toBeUndefined()
expect(JSON.stringify(requests[0]?.messages)).toContain("Manual compaction should include this short conversation.")
expect(yield* store.context(sessionID)).toMatchObject([
{ type: "compaction", reason: "manual", summary: "manual summary", recent: "" },
{
type: "compaction",
reason: "manual",
model: { id: "summary-model", providerID: "test" },
summary: "manual summary",
recent: "",
},
])
expect(
yield* db
+1
View File
@@ -488,6 +488,7 @@ export namespace Compaction {
schema: {
...Base,
reason: Started.data.fields.reason,
model: Model.Ref.pipe(optional),
text: Schema.String,
recent: Schema.String,
},
+1
View File
@@ -222,6 +222,7 @@ export const CompactionCompleted = Schema.Struct({
...CompactionBase,
status: Schema.tag("completed"),
reason: Schema.Literals(["auto", "manual"]),
model: Model.Ref.pipe(optional),
summary: Schema.String,
recent: Schema.String,
}).annotate({ identifier: "Session.Message.Compaction.Completed" })
@@ -0,0 +1,21 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { SessionMessage } from "../src/session-message.js"
describe("session message", () => {
test("decodes completed compactions recorded before model attribution", () => {
const encoded = {
type: "compaction" as const,
id: "msg_compaction",
time: { created: 0 },
status: "completed" as const,
reason: "manual" as const,
summary: "summary",
recent: "",
}
const message = Schema.decodeUnknownSync(SessionMessage.CompactionCompleted)(encoded)
expect(message.model).toBeUndefined()
expect(Schema.encodeSync(SessionMessage.CompactionCompleted)(message)).toEqual(encoded)
})
})
+2
View File
@@ -732,6 +732,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
Object.assign(current, {
status: "completed",
reason: event.data.reason,
model: event.data.model,
summary: event.data.text,
recent: event.data.recent,
})
@@ -742,6 +743,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
type: "compaction",
status: "completed",
reason: event.data.reason,
model: event.data.model,
summary: event.data.text,
recent: event.data.recent,
time: { created: event.created },
+15 -1
View File
@@ -73,7 +73,7 @@ import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keyma
import { usePathFormatter } from "../../context/path-format"
import { LocationProvider } from "../../context/location"
import { createSessionRows, resolvePart, type PartRef, type SessionRow } from "./rows"
import { switchLabel } from "../../util/model"
import { compactionMarker, switchLabel } from "../../util/model"
addDefaultParsers(parsers.parsers)
@@ -1306,10 +1306,14 @@ function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { typ
function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type: "compaction" }> }) {
const ctx = use()
const local = useLocal()
const { theme, syntax } = useTheme()
const status = () => props.message.status
const text = () => (props.message.status === "failed" ? props.message.error.message : props.message.summary)
const content = createMemo(() => text().trim())
const marker = createMemo(() =>
compactionMarker(props.message.status === "completed" ? props.message.model : undefined, ctx.models()),
)
const color = () => (status() === "failed" ? theme.error : theme.textMuted)
return (
<box>
@@ -1344,6 +1348,16 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
/>
</box>
</Show>
<Show when={marker()}>
{(value) => (
<box paddingLeft={3} marginTop={1}>
<text>
<span style={{ fg: local.agent.color(value().agent) }}>{Locale.titlecase(value().agent)}</span>
<span style={{ fg: theme.textMuted }}> · {value().model}</span>
</text>
</box>
)}
</Show>
</box>
)
}
+12
View File
@@ -7,6 +7,18 @@ export function formatRef(model: { providerID: string; id: string; variant?: str
return [model.providerID, model.id, model.variant].filter((value) => value !== undefined).join("/")
}
export function compactionMarker(
model: { providerID: string; id: string; variant?: string } | undefined,
models?: readonly { providerID: string; id: string; name: string }[],
) {
if (!model) return
return {
agent: "compaction",
model:
models?.find((item) => item.providerID === model.providerID && item.id === model.id)?.name ?? formatRef(model),
}
}
export function switchLabel(
model: { providerID: string; id: string; variant?: string },
models?: readonly { providerID: string; id: string; name: string }[],
+8 -1
View File
@@ -1126,7 +1126,13 @@ test("tracks session status from active sessions and execution events", async ()
created: 0,
type: "session.compaction.ended",
durable: durable("session-live", 5),
data: { sessionID: "session-live", reason: "auto", text: "Live summary", recent: "recent" },
data: {
sessionID: "session-live",
reason: "auto",
model: { providerID: "anthropic", id: "claude-sonnet" },
text: "Live summary",
recent: "recent",
},
})
await wait(() => {
const message = data.session.message.get("session-live", "msg_compaction_started")
@@ -1135,6 +1141,7 @@ test("tracks session status from active sessions and execution events", async ()
expect(data.session.message.get("session-live", "msg_compaction_started")).toMatchObject({
type: "compaction",
status: "completed",
model: { providerID: "anthropic", id: "claude-sonnet" },
summary: "Live summary",
})
expect(rows.find((row) => row.type === "message" && row.messageID === "msg_compaction_started")).toBe(
+15 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { formatRef, parse, switchLabel } from "../../src/util/model"
import { compactionMarker, formatRef, parse, switchLabel } from "../../src/util/model"
describe("util.model", () => {
test("splits provider from a nested model identifier", () => {
@@ -12,6 +12,20 @@ describe("util.model", () => {
expect(formatRef({ providerID: "anthropic", id: "sonnet" })).toBe("anthropic/sonnet")
})
test("labels completed compactions with their actual model", () => {
const models = [{ providerID: "anthropic", id: "sonnet", name: "Claude Sonnet" }]
expect(compactionMarker({ providerID: "anthropic", id: "sonnet" }, models)).toEqual({
agent: "compaction",
model: "Claude Sonnet",
})
expect(compactionMarker({ providerID: "removed", id: "gone", variant: "high" }, models)).toEqual({
agent: "compaction",
model: "removed/gone/high",
})
expect(compactionMarker(undefined, models)).toBeUndefined()
})
test("includes the selected variant in model switch notices", () => {
expect(switchLabel({ providerID: "anthropic", id: "sonnet", variant: "thinking" })).toBe(
"Switched model to anthropic/sonnet/thinking",