Repository navigation
fix(core): shorten compaction transcripts oldest first before dropping history - #58015
Conversation
There was a problem hiding this comment.
The transcript now shrinks in stages, keeping tool output whole, then cutting it to 1,250 and then 200 characters, and it only drops exchanges as a last resort. It works as described.
I ran the core session runner and compaction tests and the packages/core typecheck; all pass. I also ran the new runner tests against the base: the two new serialization cases fail there (an exchange is dropped where cutting tool output to 200 characters would have fit) and pass with this change. Dropping reasoning from SessionRecentText.messageToText also removes [Assistant reasoning] lines from the kept recent text, which is what #51818 reports.
The public API is unchanged. Only internal compaction request building changes; no SDK, HTTP, plugin, config or event surface is touched.
I have one question inline: whether every route accepts a transcript request with no system prompt.
| omitted ? `[${omitted} older ${omitted === 1 ? "exchange" : "exchanges"} omitted]\n\n` : "" | ||
| const transcript = (exchanges: ReadonlyArray<string>, omitted: number) => | ||
| LLMRequest.update(request, { | ||
| system: [], |
There was a problem hiding this comment.
With system: [], the Responses protocol omits instructions entirely. You checked the OpenAI compact endpoint and trigger, but this path also runs for ordinary summary streams on every provider. Have you confirmed that routes which have required a system prompt or instructions in the past (e.g. the ChatGPT sign-in route, or subscription/OAuth Anthropic) accept a request with none? If any of them reject it, the transcript fallback would fail exactly when it is needed. A minimal fixed instruction would avoid that risk.
There was a problem hiding this comment.
The new commit addresses my earlier question. The text transcript keeps the original system prompt and drops only the tool definitions, and the space calculation for dropping old exchanges now counts the system prompt. The core session runner and compaction tests (259) and the packages/core typecheck pass. The public API is unchanged. I have no further concerns.
There was a problem hiding this comment.
My earlier point about the system prompt stays addressed: the transcript still keeps it. The oldest-first shortening works as described. I checked it with the packages/core session runner and compaction tests (262 pass) and the typecheck (passes). The new and changed runner scenarios fail on the base and pass here. The public API is unchanged.
I have one small point inline: the note can undercount exchanges that disappear at the user-only level.
| } | ||
| if (total > room) return undefined | ||
|
|
||
| const omitted = levels.filter((level) => level === OMITTED).length |
There was a problem hiding this comment.
An exchange whose user-only render is empty disappears from the transcript once it is lowered to USER_ONLY. That happens when its user message is only media, or when the first group starts with an assistant message. The note doesn't count it, because only level === OMITTED is counted here, so "[N older exchanges omitted]" undercounts. Counting exchanges whose chosen render is empty while renders[0] is not (or skipping straight to OMITTED for them) would keep the note accurate.
There was a problem hiding this comment.
The new commit addresses the note undercount: exchanges that become empty at the user-only level are now counted as omitted. The packages/core session runner and compaction tests (262) and the typecheck pass. The public API is unchanged. I have no further concerns.
Problem
When a compaction request is too big to send unchanged, history gets turned into a text transcript. That step did everything at once: it kept every tool definition, cut every tool output to 1,250 characters, and if that still didn't fit, dropped whole exchanges from the oldest end. Dropped exchanges end up in neither the summary nor the kept recent messages, so the summary loses them entirely, user requests included, while newer exchanges still carry long tool output.
This hurts most when a long session moves to a model with a much smaller window, and in sessions with a lot of MCP tools, where tool definitions are a big chunk of the request the summary doesn't use.
Separately, the kept recent messages written into the checkpoint included
[Assistant reasoning]: ...lines. That text is the provider's summary of its reasoning, it inflates the checkpoint (#51818), and written out as text it can read like an attempt to extract the model's thinking.Change
The transcript (system prompt kept, no tool definitions, no reasoning, no images) is shortened one exchange at a time, oldest first, through these levels, and stops as soon as it fits:
Every exchange reaches a level before any reaches the next, so a session that's slightly over loses detail from only its oldest exchanges, and a huge one keeps every user request it can before anything is omitted. The newest exchange is never omitted. Each exchange is rendered at each level once, so fitting is running arithmetic, not repeated estimates.
The previous summary still always stays in front. After a "too long" rejection the target now steps 85%, 70%, 50%, 35% (was 70%, 50%, 35%), and each retry refits from scratch, so a small overshoot costs a small cut.
Dropping tool definitions is safe here: the transcript has no real tool calls, only text lines, and a previous native compaction window only ever holds user messages and an encrypted compaction item. I checked that OpenAI (compact endpoint and trigger) accepts the window plus a transcript with no tools. The system prompt stays, since some routes may expect one.
Reasoning is no longer written into the kept recent messages. Real messages still replay reasoning as before.
Out of scope
length.Fixes #51818
Related: #43250
Added by the opencode bot (AI-generated).
Fixes #51818
Related: #43250