Controlling Claude's context window: compaction and friends
If you’re like me, you want fine control over Claude’s context window. Maybe you’ve even experimented with aggressive forking or surgery on JSONL session files. If so, this post is for you.
Claude Code offers /compact. Hitting Esc-Esc and navigating a few turns back
in the conversation offers two more summarization options:
- Summarize up to here replaces the earlier conversation with a summary and leaves the newer messages intact.
- Summarize from here leaves the earlier conversation intact and replaces the newer messages with a summary. There is also Restore conversation, which is basically this option without a summary.
All of these operations stay within one session file. The file is an append-only
tree: each line is an entry (usually a message, sometimes bookkeeping), and most
entries have a uuid and a parentUuid. Compaction appends a special
system entry called a compact_boundary. When Claude Code loads the session,
the boundary entry tells it how to rejigger the tree to construct Claude’s
context.
The SDK provides no supported way to insert a boundary yourself. But you can stop a Query, append a hand-crafted boundary to its session file, and resume the same session. That gives you a powerful primitive: replace Claude’s context window with any ordered list of messages from the session — keep only the messages that matter, splice in your own summary before or after them, resurrect messages from an abandoned branch, or empty the context entirely.
Relinking: how the context window is built
The assistant context window — “what the model gets” in the figures below — is
determined by the last compact_boundary in the session file. The only part of
a boundary we need to care about is preservedMessages, but here are some of
the other fields you’ll see if you peek in a session file:
{
"type": "system",
"subtype": "compact_boundary",
"uuid": "<boundary-uuid>",
"parentUuid": null,
"compactMetadata": {
"preservedMessages": {
# This is where the action is.
"anchorUuid": "<another-uuid>",
"uuids": ["msg 2", "msg 3"]
}
}
}
uuids is an ordered list of earlier session entries to preserve. anchorUuid
points to either the boundary itself or a summary message appended after the
boundary.
To construct the assistant’s context window, the loader does this:
- Reparent the preserved entries immediately after the anchor, in the order listed.
- Reparent the child of the anchor message (if any) after the end of the preserved list.
- Discard entries older than the boundary that were not preserved.
That’s it. The boundary does not change the original entries in the JSONL; the loader relinks them in memory, which is why a later boundary can recover entries cut by an earlier one. The boundary message itself has no text body and does not appear in the assistant context, even if it’s the anchor.
What’s in the session file:
{anchorUuid: "anchor",
uuids: ["msg 2", "msg 3"]}What the model gets:
(Note: the boundary’s parentUuid is null. The dashed line here is
logicalParentUuid, which is ignored when constructing the context window.)
The chain is recomputed on every load, so “the child of the anchor” means whatever the anchor’s child is at load time. Entries appended after the boundary is written — a summary, or the next user turn — become the anchor’s child and slot in after the preserved list.
That one rule does all the work: both summary placements below fall out of it, and a synthetic boundary composes cleanly with whatever conversation follows it.
A boundary may preserve any ordered selection of entries, not just an existing branch of the session tree.
Examples
/compact, “up to,” “from,” empty context, and set-context are all just
different choices of anchor and preserved messages.
/compact and “Summarize up to here”: summary first
If you /compact after “msg 3”, Claude Code generates a summary message behind
the scenes and inserts it as a regular user message after the
compact_boundary. That summary message is the anchor, and the final assistant
API response (sometimes spread across multiple entries) goes into the preserved
uuids field — here we’re supposing messages 2 and 3 are the two entries of
that final response.
Note that the assistant context is different from what the UI shows you. Not only is the last assistant response preserved, but it’s after the summary:
What’s in the session file:
{anchorUuid: "summary",
uuids: ["msg 2", "msg 3"]}What the UI shows:
What the model gets:
Summarize up to here uses the same shape. The summary is the anchor, and
uuids contains all the messages after the chosen point.
“Summarize from here”: recent summary last
Suppose your conversation has 4 messages, and you rewind to message 3 with
“Summarize from here”, edit message 3 to 3v2, and then send message 5. The compact boundary uses itself as the anchor, puts
those first two messages into the preserved uuids field, and generates a
summary message which has the boundary as its parent:
What’s in the session file:
{anchorUuid: "boundary",
uuids: ["msg 1", "msg 2"]}What the model gets:
There is no special “from” algorithm and no special summary placement rule. The summary is put at the end of the preserved list simply because it was the child of the anchor and got moved after the preserved list.
A general-purpose set-context
Once you understand boundaries as splices, set-context is straightforward:
- Wait for the current assistant response to appear in the session file.
- End the Query’s input stream and wait for the Query to shut down.
- Append a boundary containing the UUIDs you want Claude to see.
- If desired, append your own summary message as a child of the boundary.
- Resume the same session in a new Query.
Choose the boundary’s anchorUuid based on where the summary belongs:
| Desired context window | anchorUuid | uuids | Summary message |
|---|---|---|---|
| summary, then selected messages | the summary | selected messages | append after boundary |
| selected messages, then summary | the boundary | selected messages | append after boundary |
| selected messages, no summary | the boundary | selected messages | omit |
| empty context window | the boundary | [] | omit |
Here’s what this might look like in practice.
What’s in the session file:
{anchorUuid: "boundary 1",
uuids: ["msg 1", "msg 2"]}{anchorUuid: "summary 2",
uuids: ["msg 1", "msg 2", "msg 3v2", "msg 5", ]}{anchorUuid: "custom preamble",
uuids: ["msg 3v2", "msg 4", "msg 7", "msg 8"]}What the model gets:
Gotchas, Caveats, Notes, and Tips
- The loader silently sanitizes the result before sending it to the API. For example, if you preserve a tool call but omit its result, the orphaned tool call is dropped. So if you want to preserve tool calls and responses, be careful to include both in the preserved message list.
- If
uuidscontains duplicates, or references entries that don’t appear earlier in the file, relinking is silently skipped and the boundary behaves as ifuuidswere empty. - The set-context recipe changes conversation history only. It can’t change the system prompt, tools, MCP servers, permissions, or filesystem state.
- The session file may lag behind the SDK’s
resultevent by ~100ms. In step 1 of the set-context recipe, make sure it’s actually flushed before shutting down your Query (e.g. poll the file on change until the result uuid is present). - In native
/compact, the relinked conversation ends with a user-role summary, so Claude Code inserts an assistant message saying “No response requested.” before accepting the next user turn. Native compaction may separately re-inject recent tool calls and results as<system-reminder>text. A synthetic boundary does not get that behavior for free. - This recipe is hacking internals and may break in the future. It was tested on Claude Agent SDK 0.3.195, whose bundled Claude Code CLI is 2.1.195.
- †If you just want to rewind to a message in active
context, you don’t need to insert a boundary at all. You can just restart
your Query and set the
resumeSessionAtoption to point to the target message. However, if you want to rewind to a message no longer in context, you have to insert a boundary entry.
Okay, you made it to the end. Thanks for coming to my office hours. Have fun managing your context windows out there.