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.

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:

MemoryServiceProxyController.java

    @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