Skip to main content
A conversation lets an end user revise a message they already sent. The revision does not overwrite the original: it is stored as a second version of that message, the reply streams on the new branch, and the conversation serves that branch from then on. The user can switch back at any time. The server owns all of the lineage. It stores the parent of every message, assembles the branch a fetch returns, and records which branch the conversation currently serves. A client holds no message tree, computes no ordering, and never sends a lineage pointer of its own. You adopt the whole capability through three touchpoints. They are additive on the wire, so a client that sends no edit_of_message_id, ignores branch, and never calls the branch route keeps exactly today’s behavior: the same payloads, the same insertion-ordered transcript.

1. Revise a message

Take a normal turn, and add edit_of_message_id naming the user message the new text replaces. message is required with it.
What the server does with it:
  • Stores the new text as a sibling version of the target message, not as a new message at the end of the transcript.
  • Leaves the superseded exchange out of the history the model sees, so the reply answers the revised message alone.
  • Streams the reply exactly like any other turn, over the same Server-Sent Events frames.
  • Serves the new branch from then on.
The conversation frame at the start of the stream carries user_message_id, the id of the message the turn just stored. Keep it if you want to offer a second revision without refetching the conversation. Only a user message can be revised. An assistant message target returns 400.

2. Render the version switcher

Fetch the conversation as usual. Every message now carries parent_message_id, and a message that has sibling versions also carries branch.
Draw branch as a version switcher under the message: the pair index and count reads as 2/2, and the previous and next controls address siblings[index - 2] and siblings[index]. Absence of the field is the whole “no fork here” signal, so branch on presence rather than counting anything yourself. The transcript a fetch returns is the branch the conversation currently serves, not every version. Superseded versions are reachable through branch.siblings and are kept in full by the conversation export, which is an audit artifact and stays version-complete.

3. Switch versions

Post the id of any message on the branch you want served. The server resolves that message’s ancestry, follows the newest reply from there, records the selection, and returns the whole conversation detail payload for that branch.
The 200 body is the same shape the conversation GET returns (conversation, messages, truncated), already assembled for the selected branch. One round trip both switches and renders, so you never preload a tree and never reconcile an optimistic swap.

What persists

The selection is stored on the conversation, not in your client:
  • A later fetch returns the selected branch.
  • The next turn continues the selected branch, and its messages are stored there.
  • A reload, a second device, or a different client of the same conversation all see the branch that was selected last.
Switching back to an earlier version lands on that branch’s newest reply.

Errors

Two client-side consequences follow from the last two rows. A turn cannot be cancelled, so disable the revise action while a turn streams rather than sending into a turn_in_progress refusal. And a confirmation gate must be resolved before a revision is accepted, so let the user answer the gate first. The branch route takes no turn claim, so a switch and a running turn are not serialized against each other. Disable the switcher while a turn streams.

Compatibility

Both new fields and the new route are additive:
  • edit_of_message_id is optional on the turn request.
  • parent_message_id is new on every message; treat it as optional if you parse responses strictly and want to keep reading older stored payloads.
  • branch is present only at a fork.
  • The branch route is new. Not calling it means the conversation keeps serving the branch its last edit created, which for a conversation with no edits is the original single thread.