Summary
This RFC evolves a chat’s retained history from a destructive linear timeline into a host-managed history graph. A client can move a chat’s current history head to an earlier turn and restore the workspace files to that turn atomically. Hosts that retain branches let the client continue from that point without deleting alternate futures; linear hosts prune those futures when the client continues.
The proposal retains activeTurn's existing streaming semantics. It gains parent metadata so a continued turn can be recorded in the history graph. The new concept is a completed-history head, named headTurnId.
Motivation
Today, chat/truncated removes turns after a selected point. A client that wants corresponding workspace rollback must independently restore files. This has several problems:
- transcript and workspace can diverge;
- file restoration is not atomic with history mutation;
- clients cannot leverage host-native mechanisms such as APFS snapshots, Git worktrees, reflinks, or filesystem journals;
- discarded work cannot be revisited or compared;
- truncation and branching are conflated;
- forks and side chats may reference stale pointers.
A conversation is more naturally a graph:
root ── T1 ── T2 ── T3 ── T4 (old head)
\
T5 ── T6 (new head after checkout at T2 and two followups)
The host owns workspace restoration and, when supported, graph persistence.
Goals
- Preserve
ChatState.activeTurn and its existing streaming semantics.
- Make restoration of workspace files host-brokered and atomic with selection of conversation history.
- Let capable hosts preserve abandoned branches until host retention policy prunes them.
- Support host-specific restoration implementations without exposing storage details.
- Provide clear concurrency and external-change behavior.
Non-goals
- Reversing external side effects such as remote API calls, deployments, or sent messages.
- Defining a general merge algorithm for conversations or workspaces.
- Requiring every host to expose all retained graph nodes to every client.
- Replacing chat forks; cross-chat forks remain useful for concurrent or isolated work.
Terminology
| Term |
Meaning |
| Turn node |
A completed turn in the host-retained conversation graph. |
| Head |
The completed turn selected as the end of the chat’s active history path. |
| Active turn |
The existing activeTurn: an in-progress, streaming turn. |
| Checkout |
Atomically selecting a graph head and restoring workspace files to that turn. |
| Selected path |
The root-to-head sequence identified by following parentTurnId from headTurnId. |
State model
ChatState.activeTurn remains unchanged.
interface ChatState {
// Existing fields
/**
* Loaded completed turns, keyed by turn ID.
*
* A host initially includes the selected path and may load other retained
* turn nodes on demand.
*/
turns: Record<string, Turn>;
activeTurn?: ActiveTurn;
/**
* ID of the completed turn at the end of the selected path.
* Omitted when the selected path is the root/baseline with no completed turns.
*/
headTurnId?: string;
}
turns is an ID-keyed collection of loaded retained turn nodes. A host SHOULD include retained branches that are relevant to its initial history view, but it need not eagerly load every descendant of a branch point. Clients render the selected linear transcript by walking parentTurnId backward from headTurnId.
The host MAY only include a subset of turns and expose more through the graph-aware fetchTurns extension described in Turn loading.
Each completed turn gains optional graph metadata:
interface Turn {
// Existing fields
id: string;
/**
* Parent completed turn in the retained graph.
* Omitted for a root-level turn.
*/
parentTurnId?: string;
}
An active turn records the same parentage until it becomes completed:
interface ActiveTurn {
// Existing fields
/**
* Completed turn selected when this turn started. Omitted when the turn
* starts from the root/baseline.
*/
parentTurnId?: string;
}
Capability negotiation
A host advertises checkout support through agent capabilities:
interface AgentCapabilities {
/** The host supports checkout and workspace restoration. */
history?: HistoryCapability;
}
interface HistoryCapability {
/**
* The host preserves alternate futures after a user continues from an
* earlier checked-out turn. When absent or `false`, the host uses linear
* history: it prunes those futures after accepting the new turn.
*/
retainsBranches?: boolean;
}
Clients MUST NOT invoke checkout unless history is advertised.
Checkout command
interface CheckoutChatHistoryParams extends BaseParams {
/** Chat channel to update. */
channel: URI;
/**
* Completed turn that becomes the new history head. Omit to select the
* chat's root/baseline state.
*/
turnId?: string;
/**
* Optional optimistic-concurrency guard. The host rejects with Conflict when
* it no longer matches the current ChatState.headTurnId. Pass `null` to
* guard the root/baseline head; omit this field to make no assertion.
*/
expectedHeadTurnId?: string | null;
/**
* Permit checkout to overwrite or delete local file changes that would not
* exist at the selected turn. Clients SHOULD set this only after presenting
* the message from `CheckoutWouldDiscardChangesErrorData` to the user.
*/
force?: boolean;
}
interface CheckoutChatHistoryResult {}
checkoutChatHistory is a request, not a client-dispatchable action. Its successful response is the synchronization boundary: once it resolves, the host has committed both selected history and restored workspace state.
Destructive-change error
The protocol adds AhpErrorCodes.CheckoutWouldDiscardChanges (-32012). Its JSON-RPC error.data is CheckoutWouldDiscardChangesErrorData:
interface CheckoutWouldDiscardChangesErrorData {
/**
* User-facing explanation of the changes that checkout would overwrite or
* delete.
*/
message: StringOrMarkdown;
}
Because JSON-RPC requires error.message to be a string, the top-level message is the fixed summary "Checkout would discard changes"; the structured, user-facing message is carried in error.data.message.
When checkout would overwrite or delete file changes and force is absent or false, the host MUST return this error without changing the chat or workspace. A client that receives it SHOULD present data.message and, after user confirmation, retry the same request with force: true. A host MUST perform the checkout when force is true, subject to normal validation and operational failures.
Checkout semantics
On receiving checkoutChatHistory, the host MUST:
- validate that the target turn belongs to the chat and is retained;
- validate
expectedHeadTurnId, when supplied;
- reject with
TurnInProgress if an activeTurn exists;
- acquire exclusive access to the workspace files it will restore;
- detect whether restoration would overwrite or delete file changes;
- return
CheckoutWouldDiscardChanges unless force is true when such changes exist;
- restore workspace files and backend conversation state to the selected turn;
- commit the selected head;
- load any missing nodes on the new selected path with
chat/turnsLoaded, then publish the authoritative updated chat state and related resource state;
- return
CheckoutChatHistoryResult.
If any step fails, the host MUST preserve the pre-request history head and workspace state. It MUST NOT report success or publish a partially applied history transition.
Hosts MUST return Conflict when a workspace lease cannot be acquired or when expectedHeadTurnId is stale. A host MAY return another applicable AHP error for operational failures that prevent restoration even with force: true.
Actions
A successful checkout emits:
interface ChatHistoryCheckedOutAction {
type: 'chat/historyCheckedOut';
headTurnId?: string;
}
This action updates the durable selected-head state on every subscribed client. No separate history-replacement action is needed: checkout loads any missing selected-path nodes first, and headTurnId plus parentTurnId identifies the selected path.
The protocol version introducing history checkout removes chat/truncated.
Linear hosts advertise history with retainsBranches absent or false and
use checkout followed by chat/turnsPruned for the destructive continuation
behavior.
Continuing after checkout
When a host accepts chat/turnStarted, it binds the new turn to the current
headTurnId. The host MUST assign this parent; a client MUST NOT choose it.
The server's authoritative chat/turnStarted action therefore adds:
interface ChatTurnStartedAction {
// Existing fields
/**
* Completed turn selected when this turn started. Omitted when the turn
* starts from the root/baseline.
*/
parentTurnId?: string;
}
When the turn completes, the host persists its parentTurnId on the completed
Turn, upserts that turn into ChatState.turns, and advances headTurnId to
the completed turn's ID.
If history.retainsBranches is absent or false, the host MUST remove
alternate futures that became unreachable when it accepted the new turn. It does so
by dispatching chat/turnsPruned after the authoritative chat/turnStarted
action:
interface ChatTurnsPrunedAction {
type: 'chat/turnsPruned';
/** IDs of completed turns removed from `ChatState.turns`. */
turnIds: string[];
}
The host MUST include every removed descendant in turnIds, MUST NOT remove
the selected path or the active turn, and MUST dispatch this action in server
order so every subscribed client converges. A host with
history.retainsBranches: true MAY also dispatch chat/turnsPruned later
when its retention policy prunes a branch.
Turn loading
No branch-discovery request is needed. Graph-aware clients use the existing fetchTurns command to page the direct children of a known turn. Passing parentTurnId: null requests root-level turns; passing an ID requests the direct children of that turn. This lets a client expand a branch point on demand without loading a long, unrelated run of descendant turns.
interface FetchTurnsParams extends BaseParams {
channel: URI;
/**
* Opaque cursor from a previous `fetchTurns` result. The cursor encodes the
* requested parent; callers MUST NOT combine it with `parentTurnId`.
*/
cursor?: string;
/**
* When supplied, load a page of this turn's direct children. `null` selects
* root-level turns. A caller starts a child-page request with this field,
* then follows the returned cursor until it is absent.
*/
parentTurnId?: string | null;
}
interface FetchTurnsResult {
/**
* Opaque cursor for the next page of the requested children, if one remains.
* Omitted when every direct child of the requested parent has been loaded.
*/
nextCursor?: string;
}
The command remains action-backed: before responding, the host dispatches chat/turnsLoaded with the loaded nodes:
interface ChatTurnsLoadedAction {
type: 'chat/turnsLoaded';
/** Turn nodes to upsert into `ChatState.turns`, keyed by turn ID. */
turns: Record<string, Turn>;
}
The reducer upserts the supplied nodes into ChatState.turns. FetchTurnsResult.nextCursor continues the same child-page request, so clients can page one branch point independently of other branches. Clients retain a returned cursor only for as long as they need to continue that page; after reconnecting, they can begin a new request for the relevant parent.
Retention
Hosts MAY prune retained branches according to policy. A host MUST NOT expose a retained turn as eligible for checkout unless it can restore the workspace files to that turn. A host MUST NOT prune a turn that is referenced by a fork.
Hosts SHOULD prune a node and its unreferenced descendants together, unless they can preserve a valid re-rooted representation.
Summary
This RFC evolves a chat’s retained history from a destructive linear timeline into a host-managed history graph. A client can move a chat’s current history head to an earlier turn and restore the workspace files to that turn atomically. Hosts that retain branches let the client continue from that point without deleting alternate futures; linear hosts prune those futures when the client continues.
The proposal retains
activeTurn's existing streaming semantics. It gains parent metadata so a continued turn can be recorded in the history graph. The new concept is a completed-history head, namedheadTurnId.Motivation
Today,
chat/truncatedremoves turns after a selected point. A client that wants corresponding workspace rollback must independently restore files. This has several problems:A conversation is more naturally a graph:
The host owns workspace restoration and, when supported, graph persistence.
Goals
ChatState.activeTurnand its existing streaming semantics.Non-goals
Terminology
activeTurn: an in-progress, streaming turn.parentTurnIdfromheadTurnId.State model
ChatState.activeTurnremains unchanged.turnsis an ID-keyed collection of loaded retained turn nodes. A host SHOULD include retained branches that are relevant to its initial history view, but it need not eagerly load every descendant of a branch point. Clients render the selected linear transcript by walkingparentTurnIdbackward fromheadTurnId.The host MAY only include a subset of
turnsand expose more through the graph-awarefetchTurnsextension described in Turn loading.Each completed turn gains optional graph metadata:
An active turn records the same parentage until it becomes completed:
Capability negotiation
A host advertises checkout support through agent capabilities:
Clients MUST NOT invoke checkout unless
historyis advertised.Checkout command
checkoutChatHistoryis a request, not a client-dispatchable action. Its successful response is the synchronization boundary: once it resolves, the host has committed both selected history and restored workspace state.Destructive-change error
The protocol adds
AhpErrorCodes.CheckoutWouldDiscardChanges(-32012). Its JSON-RPCerror.dataisCheckoutWouldDiscardChangesErrorData:Because JSON-RPC requires
error.messageto be a string, the top-level message is the fixed summary"Checkout would discard changes"; the structured, user-facing message is carried inerror.data.message.When checkout would overwrite or delete file changes and
forceis absent orfalse, the host MUST return this error without changing the chat or workspace. A client that receives it SHOULD presentdata.messageand, after user confirmation, retry the same request withforce: true. A host MUST perform the checkout whenforceistrue, subject to normal validation and operational failures.Checkout semantics
On receiving
checkoutChatHistory, the host MUST:expectedHeadTurnId, when supplied;TurnInProgressif anactiveTurnexists;CheckoutWouldDiscardChangesunlessforceistruewhen such changes exist;chat/turnsLoaded, then publish the authoritative updated chat state and related resource state;CheckoutChatHistoryResult.If any step fails, the host MUST preserve the pre-request history head and workspace state. It MUST NOT report success or publish a partially applied history transition.
Hosts MUST return
Conflictwhen a workspace lease cannot be acquired or whenexpectedHeadTurnIdis stale. A host MAY return another applicable AHP error for operational failures that prevent restoration even withforce: true.Actions
A successful checkout emits:
This action updates the durable selected-head state on every subscribed client. No separate history-replacement action is needed: checkout loads any missing selected-path nodes first, and
headTurnIdplusparentTurnIdidentifies the selected path.The protocol version introducing history checkout removes
chat/truncated.Linear hosts advertise
historywithretainsBranchesabsent orfalseanduse checkout followed by
chat/turnsPrunedfor the destructive continuationbehavior.
Continuing after checkout
When a host accepts
chat/turnStarted, it binds the new turn to the currentheadTurnId. The host MUST assign this parent; a client MUST NOT choose it.The server's authoritative
chat/turnStartedaction therefore adds:When the turn completes, the host persists its
parentTurnIdon the completedTurn, upserts that turn intoChatState.turns, and advancesheadTurnIdtothe completed turn's ID.
If
history.retainsBranchesis absent orfalse, the host MUST removealternate futures that became unreachable when it accepted the new turn. It does so
by dispatching
chat/turnsPrunedafter the authoritativechat/turnStartedaction:
The host MUST include every removed descendant in
turnIds, MUST NOT removethe selected path or the active turn, and MUST dispatch this action in server
order so every subscribed client converges. A host with
history.retainsBranches: trueMAY also dispatchchat/turnsPrunedlaterwhen its retention policy prunes a branch.
Turn loading
No branch-discovery request is needed. Graph-aware clients use the existing
fetchTurnscommand to page the direct children of a known turn. PassingparentTurnId: nullrequests root-level turns; passing an ID requests the direct children of that turn. This lets a client expand a branch point on demand without loading a long, unrelated run of descendant turns.The command remains action-backed: before responding, the host dispatches
chat/turnsLoadedwith the loaded nodes:The reducer upserts the supplied nodes into
ChatState.turns.FetchTurnsResult.nextCursorcontinues the same child-page request, so clients can page one branch point independently of other branches. Clients retain a returned cursor only for as long as they need to continue that page; after reconnecting, they can begin a new request for the relevant parent.Retention
Hosts MAY prune retained branches according to policy. A host MUST NOT expose a retained turn as eligible for checkout unless it can restore the workspace files to that turn. A host MUST NOT prune a turn that is referenced by a fork.
Hosts SHOULD prune a node and its unreferenced descendants together, unless they can preserve a valid re-rooted representation.