Conversation Forking
This guide covers conversation forking — letting users branch off from any point in a conversation to explore alternative paths.
New to forking concepts? Read Forking first to understand how conversation forking works. This guide focuses on the Spring implementation.
Prerequisites
Starting checkpoint: This guide starts from java/spring/examples/doc-checkpoints/03-with-history
Make sure you’ve completed the previous guides:
- Getting Started - Basic memory service integration
- Conversation History - History recording and APIs
Conversation Forking
How Forking Works
Forks are created implicitly when the first entry is appended to a new conversation with fork metadata. In this checkpoint, the ChatController accepts optional fork query params and forwards them to the history advisor:
this.historyAdvisorBuilder = historyAdvisorBuilder;
this.authorizedClientService = authorizedClientServiceProvider.getIfAvailable();
}
@PostMapping("/chat/{conversationId}")
public String chat(
@PathVariable String conversationId,
@RequestParam(required = false) String forkedAtConversationId,
@RequestParam(required = false) String forkedAtEntryId,
@RequestBody String message) {
String bearerToken = SecurityHelper.bearerToken(authorizedClientService);
var chatMemoryAdvisor =
MessageChatMemoryAdvisor.builder(
MessageWindowChatMemory.builder()
.chatMemoryRepository(repositoryBuilder.build(bearerToken))
.build())
.build();
var historyAdvisor = historyAdvisorBuilder.build(bearerToken);
var chatClient =
chatClientBuilder
.clone()
.defaultSystem("You are a helpful assistant.")
.defaultAdvisors(historyAdvisor, chatMemoryAdvisor)
.defaultAdvisors(
advisor -> {
advisor.param(ChatMemory.CONVERSATION_ID, conversationId);
if (forkedAtConversationId != null
&& !forkedAtConversationId.isBlank()) {
advisor.param(
ConversationHistoryStreamAdvisor
.FORKED_AT_CONVERSATION_ID_KEY,
forkedAtConversationId);
}
if (forkedAtEntryId != null && !forkedAtEntryId.isBlank()) {
advisor.param(
ConversationHistoryStreamAdvisor
.FORKED_AT_ENTRY_ID_KEY,
forkedAtEntryId);
}
}) Why: Passing forkedAtConversationId and forkedAtEntryId through advisor context lets the same chat endpoint create either a root conversation or a fork, while still recording fork metadata on the first USER turn.
This guide keeps the example history-based. Trusted runtime clients can also fork at client-visible journal entries for replay/debug workflows; see Forking for the channel rules.
Listing Forks
Add this method to the MemoryServiceProxyController to list forks for a conversation:
@GetMapping("/{conversationId}/forks")
public ResponseEntity<?> listConversationForks(@PathVariable String conversationId) {
return proxy.listConversationForks(conversationId); What changed: A new GET /{conversationId}/forks endpoint is added to MemoryServiceProxyController, delegating to proxy.listConversationForks().
Why: Frontend UIs receive all accessible group IDs plus fork points keyed to entries visible in the requested conversation. Exposing the snapshot through the Spring proxy keeps Memory Service behind the application while entries are paged separately.
Try It With Curl
Define a helper to get a bearer token for bob:
function get-token() {
curl -sSfX POST http://localhost:8081/realms/memory-service/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=memory-service-client" \
-d "client_secret=change-me" \
-d "grant_type=password" \
-d "username=bob" \
-d "password=bob" \
| jq -r '.access_token'
}Create a turn on the source conversation:
curl -NsSfX POST http://localhost:9090/chat/a8391789-592d-4273-90df-7349b08b5d3d \
-H "Content-Type: text/plain" \
-H "Authorization: Bearer $(get-token)" \
-d "Hello from the root conversation." Example output:
Sure, I can help with that. Fetch the entry id to fork from:
curl -sSfX GET http://localhost:9090/v1/conversations/a8391789-592d-4273-90df-7349b08b5d3d/entries \
-H "Authorization: Bearer $(get-token)" | jq Example output:
{
"data": [
{
"id": "7b49adf0-ff34-4019-937d-258d9d154c93"
},
{
"id": "6a3d51d1-d4fb-482e-af40-74dfb9ae13e1"
}
],
"afterCursor": null
} Create the forked conversation by calling chat with fork metadata:
curl -NsSfX POST "http://localhost:9090/chat/6dcb8f3f-bd21-459f-9127-96e5a94d276c?forkedAtConversationId=a8391789-592d-4273-90df-7349b08b5d3d&forkedAtEntryId=${FORK_ENTRY_ID}" \
-H "Content-Type: text/plain" \
-H "Authorization: Bearer $(get-token)" \
-d "Continue from this fork." Example output:
{
"data": [
{
"id": "7b49adf0-ff34-4019-937d-258d9d154c93",
"conversationId": "a8391789-592d-4273-90df-7349b08b5d3d",
"userId": "bob",
"channel": "history",
"epoch": null,
"contentType": "history",
"content": [
{
"role": "USER",
"text": "Hello from the root conversation."
}
],
"createdAt": "2026-03-06T14:59:24.681546Z"
},
{
"id": "6a3d51d1-d4fb-482e-af40-74dfb9ae13e1",
"conversationId": "a8391789-592d-4273-90df-7349b08b5d3d",
"userId": "bob",
"channel": "history",
"epoch": null,
"contentType": "history",
"content": [
{
"role": "AI",
"text": "Hello! How can I assist you today?"
}
],
"createdAt": "2026-03-06T14:59:26.097352Z"
}
],
"afterCursor": null
} List forks for the source conversation through the Spring proxy endpoint:
curl -sSfX GET http://localhost:9090/v1/conversations/a8391789-592d-4273-90df-7349b08b5d3d/forks \
-H "Authorization: Bearer $(get-token)" | jq Example output:
{
"conversationIds": [
"a8391789-592d-4273-90df-7349b08b5d3d",
"6dcb8f3f-bd21-459f-9127-96e5a94d276c"
],
"forkPoints": [
{
"entryId": "6a2f66b8-6d31-41ee-9c17-0c592af6ef8f",
"options": [
{
"conversationId": "a8391789-592d-4273-90df-7349b08b5d3d",
"entryId": "6a2f66b8-6d31-41ee-9c17-0c592af6ef8f",
"title": "A conversation",
"createdAt": "2026-07-10T14:00:00Z"
},
{
"conversationId": "6dcb8f3f-bd21-459f-9127-96e5a94d276c",
"entryId": "4884589c-c68a-4e13-a73d-f51715be6e33",
"title": "Forked conversation",
"createdAt": "2026-07-10T14:01:00Z"
}
]
}
]
} Next Steps
- Response Recording and Resumption - Streaming responses with resume and cancel support.
- Conversation Sharing - Share conversations with other users.
- Docker Compose Integration - Automatic memory-service container startup for development and testing.