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:

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:

ChatController.java
        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.

Listing Forks

Add this method to the MemoryServiceProxyController to list forks for a conversation:

MemoryServiceProxyController.java

    @GetMapping("/{conversationId}/forks")
    public ResponseEntity<?> listConversationForks(@PathVariable String conversationId) {
        return proxy.listConversationForks(conversationId, null, null);

What changed: A new GET /{conversationId}/forks endpoint is added to MemoryServiceProxyController, delegating to proxy.listConversationForks().

Why: Frontend UIs need to discover all conversations that branched off from a given point so they can render a fork tree or allow users to navigate between branches. Exposing this as a proxied endpoint keeps the Memory Service behind your Spring Boot app while giving the frontend everything it needs to build a branching conversation UI.

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:

{
  "data": [
    {
      "conversationId": "a8391789-592d-4273-90df-7349b08b5d3d"
    },
    {
      "conversationId": "6dcb8f3f-bd21-459f-9127-96e5a94d276c"
    }
  ],
  "afterCursor": null
}

Next Steps