The transcript is untouched
This is the part worth being precise about. Compaction cuts what the model is shown, never what a person reads. Scroll to the top of a conversation that has been summarised three times and every message you or your model ever sent is still there, in order, exactly as it was written. Nothing is deleted and nothing is rewritten in place. What changes is the next prompt. From the cut point back, the model reads a summary instead of the messages themselves; from the cut point forward, it reads the messages verbatim, same as always. The two views (what a person sees, what the model is sent) are allowed to diverge, and compaction is the one thing in this platform that deliberately makes them diverge.It is billed as ordinary inference
Summarising a conversation is itself a call to a model, on the conversation’s own model: a cheaper pinned model would send your conversation to a sub-processor your workspace never chose. That call is metered and billed exactly like the turn you sent, no separate rate, no surcharge for the feature. It appears on your usage page as its own line, so a conversation that has been summarised several times shows what each pass cost rather than folding it into the turn that happened to trigger it.Where the summary begins
GET /chat/v1/threads/{thread} carries a compaction field on every read:
first_kept_message_uuid is the first message in the transcript still sent to the model verbatim; the summary stands in for everything before it. created_at is when that summary was written. A client draws one line above that message, the same way it would draw any other marker in the transcript: everything above the line is history a person can still read but the model no longer sees whole, everything below it is exactly what the model was sent.
The field is null on a conversation that has never been compacted, never absent: a client reads it the same way on every conversation without first checking whether this one happens to have a summary.
first_kept_message_uuid can itself be null inside a non-null compaction. The message it names is not kept forever just because a summary points at it, and a conversation’s normal retention can remove it later. When that happens the marker has nowhere left to point: render it as “this conversation was summarised” without the line, because the message the summary resumes at is gone.
When even the summary does not fit
This is the automatic case: a turn compacting on its own, inside a run, because it crossed the threshold on its own. A turn that cannot fit inside the model’s window, even after compacting, fails withcontext_exhausted. There is nothing left to cut on that model; the remedy is to start a new conversation, or pick a model with a larger window.
The summarising call this automatic path makes can fail too, independently, and its failures carry the same word every other refusal on that turn would, with a compaction_ prefix, so a client can tell that the summariser was refused rather than the turn itself: compaction_context_exhausted when what is being summarised does not fit even in the summariser’s own call, compaction_rate_limit_error when the service is at capacity and the summarising call could not be made, compaction_pii_blocked when your workspace refuses a kind of personal data the messages being summarised carry, compaction_empty_summary when the model answered with nothing at all, and compaction_summary_refused when the prompt-injection check refused the summary the model wrote. The last two leave the conversation exactly as it was: nothing is summarised and nothing is cut, so the next turn is free to try again. Any other pipeline refusal reaching that call (an exhausted balance, a provider fault) is prefixed the same way, for the same reason: a bare code here would be indistinguishable from the same refusal on the message you just sent.
A long conversation is summarised in more than one call, and you will not notice. The summariser reads a bounded amount of the oldest history at a time, so a conversation far past the threshold is compacted in a few passes on the same turn rather than in one enormous call. This is invisible: the conversation carries one summary, the newest, and it covers everything the earlier ones did. It is bounded that way because one call large enough to swallow a whole conversation is a call no provider’s per-minute budget will serve, which used to leave a thread at once too large to send and too large to compact.
Those passes stop when the conversation is small enough to actually be asked, not merely small enough for the window. Both measures from the top of this page decide when to stop, exactly as they decide when to start: summarising until only the window looked satisfied is what used to leave a conversation looking finished while the model still refused it. A conversation that crosses the threshold in the ordinary way is summarised once and carries on.
The compaction_ prefix belongs to this automatic path alone. The endpoint below, which you call on purpose, never uses it: see its own refusals.
Compacting on purpose
A person can ask for a summary before the wall forces one: POST /chat/v1/threads//compact There is no body. Asking for a compaction is the whole request, and there is nothing to configure: this is a button, not a form. Nobody reads a summary, and nobody steers one. There is no way to say what a summary should keep, and the summary itself is never served: compaction is meant to be invisible, and what the model is shown from here is the platform’s business rather than a document to review. It answers with the marker, which is the same pair the thread read carries, so a client draws the line the same way whichever call it made:uuid identifies the compaction itself. It is a handle, not content: it is what
you quote when you ask us about a particular compaction, and there is nothing
behind it you can read.
This endpoint makes one bounded pass, so on a very long conversation it can answer 200 with the conversation still above the threshold. The summariser reads a bounded amount of the oldest history per call, exactly as the automatic path does, and unlike the automatic path this one does not repeat: a person is waiting on the response. If the conversation is still long enough to matter, press it again. Nothing is lost by doing so, and each press folds the previous summary into the new one.
Every refusal this endpoint answers on its own carries its own code, in its own words, and none of them carry the compaction_ prefix above: that prefix belongs to the automatic path, and this one answers synchronously, in the response body, rather than as a run’s terminal state.
Any other refusal the pipeline itself makes (an exhausted balance, a provider fault) passes through exactly as it would for an ordinary turn, with its own status code, because this endpoint has no better answer for those than the pipeline’s own.