posit-dev / posit-dev/shinychat

Greeting message is mis-ordered when restoring a conversation from history

Open
#306 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

ai-triage:done Priority: Medium
Dominant language
TypeScript
Stars
139
Forks
28
Avg merge
23h 44m
Merged PRs (30d)
50

Description

Summary

If a chat starts with an assistant "greeting" message (via chat_ui(messages = ...), chat_append(), or chat_server(greeting = ...)) and the user then has at least one real exchange, navigating away via the history navigator and back reorders the greeting so it lands after the user's first message instead of before it.

Expected order: [greeting (A0), user (U1), response (A1)]
Actual order after restore: [user (U1), greeting (A0), response (A1)]

Repro app

library(shiny)
library(bslib)
library(shinychat)
library(ellmer)

setClass(
  "ReproEchoProvider",
  representation(name = "character", model = "character")
)

make_echo_client <- function() {
  stored_turns <- list()
  client <- list(
    get_turns = function() stored_turns,
    set_turns = function(value) {
      stored_turns <<- value
      invisible(client)
    },
    get_tools = function() list(),
    get_provider = function() {
      methods::new("ReproEchoProvider", name = "Echo", model = "echo-test")
    },
    get_model = function() "echo-test",
    clone = function() make_echo_client()
  )
  class(client) <- c("Chat", "R6")
  client
}

record_exchange <- function(client, user_text, assistant_text) {
  turns <- c(
    client$get_turns(),
    list(
      UserTurn(contents = list(ContentText(user_text))),
      AssistantTurn(contents = list(ContentText(assistant_text)))
    )
  )
  client$set_turns(turns)
}

ui <- page_fillable(
  chat_ui(
    "chat",
    fill = TRUE,
    messages = list("Hi! I'm a greeting message."),
    placeholder = "Type a message"
  )
)

server <- function(input, output, session) {
  client <- make_echo_client()

  chat_enable_history(
    "chat",
    client,
    options = history_options(
      store = "memory",
      scope = "repro-user",
      title = NULL
    )
  )

  observeEvent(input$chat_user_input, {
    user_text <- input$chat_user_input
    if (is.list(user_text)) user_text <- user_text[[1]]
    assistant_text <- paste0("echo: ", user_text)
    record_exchange(client, user_text, assistant_text)
    chat_append("chat", assistant_text)
  })
}

shinyApp(ui, server)

Steps:

  1. Load the app - greeting shows: "Hi! I'm a greeting message."
  2. Send a message, wait for the echoed response.
  3. Open the history navigator, click "New conversation".
  4. Open the history navigator again, click back into the first conversation.
  5. Observe the greeting is now the second message instead of the first.

Verified with a scripted shinytest2 run driving the actual history navigator UI (trigger button, "New conversation", conversation list item) - not just a code-level inference. Resulting order after restore: ["Tell me a joke", "Hi! I'm a greeting message.", "echo: Tell me a joke"].

Likely culprit

The greeting is UI-only - it's rendered client-side and never becomes an ellmer turn (chat_append(), chat_ui(messages = ...), and the chat_server(greeting = ...) machinery all funnel into UI-only message sends; client$add_turn() or equivalent is never called for it).

https://github.com/posit-dev/shinychat/blob/2570b8d1025d5c7cc6a31a57c9e61129c4bc9e28/pkg-r/R/chat_history_types.R#L241-L323

extend_record_linear() builds one history-tree node per ellmer turn-group, then separately distributes the browser's reported UI messages onto those nodes:

  • "user"-role UI messages are matched to the corresponding user-turn node.
  • Everything else falls through to fallback, defined as the last newly created node.

Since the greeting has no backing turn, it's turn-less and gets swept into fallback - which, once a real exchange exists, is the node for the most recent assistant turn - rather than getting its own leading node. That's why it ends up appended right before/after the real response instead of staying in its original leading position.

Other relevant call sites:

https://github.com/posit-dev/shinychat/blob/2570b8d1025d5c7cc6a31a57c9e61129c4bc9e28/pkg-r/R/chat_history.R#L76-L153

on_response() - supplies recorded_turns (ellmer turns only) and messages/get_reported_messages() (full UI list including the greeting) to extend_record_linear().

https://github.com/posit-dev/shinychat/blob/2570b8d1025d5c7cc6a31a57c9e61129c4bc9e28/pkg-r/R/chat_history.R#L235-L297

replay_ui() - walks the node path in order and flattens each node's ui list, which is where the reordering becomes visible.

https://github.com/posit-dev/shinychat/blob/2570b8d1025d5c7cc6a31a57c9e61129c4bc9e28/pkg-r/R/chat.R#L734-L746

chat_append() - confirms the greeting never touches the ellmer client's turn history.

This isn't a persistence bug (the greeting is saved via FileConversationStore$put()) - it's a node-assignment bug in extend_record_linear()'s heuristic for placing turn-less UI messages. A fix likely needs to special-case UI messages reported before the first turn-bearing node (anchor them to a dedicated pre-turn/root node) rather than defaulting them to fallback. Leaving the exact approach open for whoever picks this up.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with extend_record_linear() in pkg-r/R/chat_history_types.R, then trace how on_response() in pkg-r/R/chat_history.R supplies messages and how replay_ui() restores them. Reproduce the issue with the supplied Shiny app or scripted shinytest2 flow, and verify that a greeting remains before the first user message after restoring a conversation.

Written by the indexing model from the issue text.

Assessment

Tech stack
r
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
67/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.