Blog • AI / API

Claude API 2026: What Is Conversation Compaction? How Does an AI Agent Persist Long Context and Compacted State as JSON?

The longer an Agent runs, the longer the Messages array gets. Tool results, thinking blocks, and user back-and-forth all fill the context window. In 2026 the Messages API made this an official capability: Conversation Compaction.

Compaction is not a summary you write on the client and paste back. The API issues a type: "compaction" JSON block. The next request must put that block back into messages unchanged, or the session will not continue from the compacted state.

This article covers:

Keep this in mind: What you persist is not “the full conversation.” It is the latest compaction block, plus the turns after it that have not been eaten. In the block, content, signature, and encrypted_content are the contract, not comments. Below we split the two compaction paths → what the block looks like → the persist list → the swap rules.

Which span Compaction actually compresses

The official stance is clear: server-side compaction is the recommended approach for long conversations and Agent workflows. You do not have to write summary code on the client. When the window fills with old content, response quality drops. Compaction swaps earlier content for a summary so the useful context can keep moving. See

Claude API Compaction.

Once threshold compaction is on, the API does four steps: input tokens hit the trigger; it writes a summary; it emits a compaction block; it continues the reply on the compacted context. Next time you append the full assistant response to messages, the API drops everything before that block and continues from the summary.

It is lossy. Tool arguments that were used, exact wording, and early thinking are not kept word for word. What remains are goals, decisions, and facts you will still need. That is not the same as “store the entire messages array in a database.” How context grows in an Agent stack: see

2026 AI Agent Stack.

Two paths: threshold compaction and on-demand compaction

2026 has two betas. Do not mix them in the same request. Official docs say the compaction parameter cannot be sent with context_management.

Path Beta header What you write in the JSON Where the block appears
Threshold compaction compact-2026-01-12 context_management.edits plus compact_20260112 At the start of an ordinary reply; text can follow
On-demand compaction compact-2026-09-04 Top-level compaction: {"type":"summarize"} A standalone response; stop_reason is compaction; no ordinary reply

Threshold compaction’s trigger defaults to input_tokens 150000, minimum 50000. Optional pause_after_compaction: after the summary is written it stops first, so you can put recent turns or instructional messages back after the block, then continue generation. A custom instructions replaces the default summary prompt in full, up to 16384 characters.

On-demand compaction is the September line’s main point. You decide when to compact; the summarize request can run in the background while the conversation keeps the full history; you can also leave the last few turns in the original after the summary. The signed block must sit at the front of messages. Messages that were summarized must be removed from in front of the block, or you get a 400 named compaction_block_misplaced. Bedrock and Google Cloud do not have the on-demand path.

Messages JSON before and after compaction

Look at the before state first: ordinary user / assistant turns.

On-demand compaction request (excerpt from the official docs)
{
  "model": "claude-opus-5",
  "max_tokens": 4096,
  "messages": [
    {
      "role": "user",
      "content": "I am building a recipe app. Help me name the main entities in the data model."
    },
    {
      "role": "assistant",
      "content": "Start with Recipe, Ingredient, and Step."
    },
    {
      "role": "user",
      "content": "Good. Now suggest field names for Recipe."
    }
  ],
  "compaction": { "type": "summarize" }
}

The response is a single signed compaction block; stop_reason is compaction. On every later request that block must sit first, followed only by turns this summary did not eat.

Continue after compaction: the block is first, old messages are gone
{
  "model": "claude-opus-5",
  "max_tokens": 2048,
  "messages": [
    {
      "role": "assistant",
      "content": [
        {
          "type": "compaction",
          "content": "Summary of the conversation: the user is designing the data model for a recipe app...",
          "signature": "EuYBCkQY..."
        }
      ]
    },
    {
      "role": "assistant",
      "content": "For Recipe, use title, description, servings..."
    },
    {
      "role": "user",
      "content": "Now do the same for Ingredient."
    }
  ]
}

A threshold-compaction block sits more “inside the reply”: the assistant’s content array starts with compaction, then text. After several compactions, the last block wins; it is the final state of the current prompt. When the API sees that block, it ignores content blocks before it. You can echo the whole thing, or drop the old messages yourself and send only from that block forward.

Field What it is Can you change it
type Fixed compaction No
content The summary text; on failure, null Do not edit the text; an empty string is illegal
signature The signature returned by on-demand compaction Must be echoed unchanged
encrypted_content Opaque metadata; the SDK type requires a verbatim echo If present, store it unchanged. Do not parse it.
cache_control Optional. A separate cache breakpoint for the summary. You add it. It is not an issued field.

The SDK describes encrypted_content as “opaque metadata left by a previous compaction; it must be round-tripped verbatim.” Do not unpack it on the client, and do not show it as the summary body. When a failed block’s content is null, the server treats it as a no-op. You can still send it back, but it will not count as a valid summary.

Fields you must persist to restore a session

Once the Agent restarts, the in-memory messages is gone. What you restore is not “the whole chat log from before compaction.” It is the state that can still make the next request after compaction.

  1. 1
    The latest compaction block

    The whole JSON block, including content, signature, and encrypted_content. If you have several, keep only the latest: the next compaction eats the old summary and everything after it.

  2. 2
    messages after the block

    user / assistant turns this summary did not cover, in the original order. Turns that grew during background compaction sit here too.

  3. 3
    The system and tools from that moment

    On on-demand compaction, the summarize request must carry the same system and tools as the rest of the conversation. On models that keep thinking, whether thinking on the turns after the block stays valid depends on whether those two changed, and on whether the tail sits right against the summarized messages.

  4. 4
    Which beta you are on

    Threshold compaction keeps sending compact-2026-01-12. For an on-demand block, every later request must still send compact-2026-09-04. If the header is wrong, the block may not be recognized.

A minimal session persist shape (illustrative, not an official schema)
{
  "beta": "compact-2026-09-04",
  "model": "claude-opus-5",
  "system": "You are a coding agent.",
  "tools": [],
  "compaction_block": {
    "type": "compaction",
    "content": "Summary of the conversation: ...",
    "signature": "EuYBCkQY...",
    "encrypted_content": "..."
  },
  "tail": [
    {
      "role": "assistant",
      "content": "For Recipe, use title, description..."
    },
    {
      "role": "user",
      "content": "Now do the same for Ingredient."
    }
  ]
}

When you rebuild the request, put in messages[0] the assistant that carries this block (or put the block in the first content of the first message), then append tail. Do not stuff old user messages from before compaction back in front of the block.

This is not the same layer as permission evaluation. That side decides whether a tool call may run. This side decides whether the state is still there after the window is eaten. For permissions, see

How Claude Managed Agents Decide Whether a Tool Call Is Allowed.

How Claude Joins AI R&D Workflows.

How to swap during background compaction and when you keep a tail

An on-demand compaction request only summarizes the messages it received. So you can send only the old turns to be compacted, keep the last few turns in the original on the client, and insert the block in front of those turns when it comes back. Officially this is keep-tail.

Background compaction (async) goes further: while the summary is still running, the conversation keeps using the full history. When it returns you cannot replace the whole history with one block. You only drop from the front the messages you sent to be compacted, put the returned assistant message in, and leave the new turns that grew after that as they are.

Background compaction swap (official illustration)
if response.stop_reason == "compaction":
    compaction_message = {
        "role": "assistant",
        "content": response.content,
    }
    history = [compaction_message] + history[sent_count:]
# sent_count = number of messages sent in the compaction request

Do not edit messages already in the history in the middle. The swap must happen on the first request after the block arrives, so thinking produced during summarization stays valid. If no block comes back, keep the full history and try again later.

If threshold compaction turns on pause_after_compaction, the response is often just that block, and stop_reason is also compaction. Append the original turns you want to keep after the block, then send the next request. Note: on Claude Fable 5.1, if you put old assistant turns back after the block, strip their thinking / redacted_thinking, or send thinking-binding-controls-2026-08-01 and set prefix_mismatch_behavior: "drop_block", or you may get a 400. Those thinking blocks were generated against the full history and will not match the compacted conversation.

How it stacks with prompt cache

Compaction and prompt caching can run together. You can put cache_control: {"type":"ephemeral"} on the compaction block so the summary is cached on its own. Add another breakpoint at the end of the system prompt. When compaction happens, the system-prompt cache can still hit, and only the summary segment is rewritten.

For billing, do not look only at top-level usage. Tokens from compaction iterations may be recorded in usage.iterations. On the persist side you still only care about the block and the tail. Do not treat usage as the contract for restoring a session.

Check the block and the history in JSONNote

JSON size before and after compaction differs a lot, and you cannot scan the fields by eye. When debugging, check position, not prose.

  1. 1
    Format the persisted file first

    Drop the session JSON into JSON Formatter. Confirm that compaction_block.type is compaction and that signature is still there.

  2. 2
    Then validate the block’s shape

    Use JSON Schema to write a minimal schema: type enumerates only compaction; content allows string or null; if signature / encrypted_content is present, treat it as string. Empty strings are forbidden.

  3. 3
    Diff messages before and after compaction

    Use JSON Diff to see which turns left the front of the queue and what appeared after the block. After on-demand compaction, summarized user messages should not sit in front of the block.

  4. 4
    Redact signatures when you recap

    The summary body can be shown to people. Redact signatures and opaque fields before Hash share. They can resume a session. Do not send them as ordinary text.

FAQ

Does Conversation Compaction delete the whole conversation?

No. The API replaces already-compacted old messages with a summary. The summary sits in a content block whose type is compaction. You must send that block back unchanged, or the session cannot continue from the summary.

What is the minimum JSON you must store to restore a session?

At least the latest compaction block (including content, signature, encrypted_content), every message after the block, and the system and tools from that moment. Drop the signature or an opaque field, and the next request will not connect.

How do threshold compaction and September on-demand compaction differ?

Threshold compaction uses compact-2026-01-12 and fires by token count inside an ordinary request. On-demand compaction uses compact-2026-09-04, sends compaction: {type: summarize} on its own, and returns a signed block that must sit at the front of messages.

Can a summary you write yourself replace a compaction block?

Not as the same contract. The platform recognizes the block it issued. A user text you wrote is just another message. It has no signature, and it will not make the API drop the old content in front of the block.

Can you change a compaction block’s content and send it back?

Do not change it. Official docs require a verbatim echo. An empty-string content is illegal; content of null means compaction failed, and the server treats it as a no-op. Changing signature or encrypted_content makes later requests fail or drop the summary.

Summary

Conversation Compaction turns “the window is about to burst” into a JSON block you can send back. Threshold compaction happens automatically inside an ordinary request. September on-demand compaction lets you decide when to compact, whether it can run in the background, and whether the last few turns stay in the original.

Restoring a session depends on the latest compaction block plus the tail after it, not the full text from before compaction. Persist content, signature, and encrypted_content unchanged, and send them unchanged. If the block is in the wrong place, the API will not pretend you still have that history.

Read the block and the tail as two contracts. For long tasks a person must watch, write the entities that must remain and the open questions into the summary instructions. For an Agent that should be interrupted less, swap the block in the background with on-demand compaction. Do not keep only a note you wrote yourself in memory.

← Back to blog