Skip to content

Message sent during a failed stream silently forks the chat into two concurrent branches with tool access #296

Description

@Classic298
A message sent while a turn is still streaming can split the chat into two independent branches that both run model turns with tool access at the same time. The user sees one conversation, but a second, invisible branch is processing their messages, calling tools and editing files, and neither the UI thread nor the model context of the other branch ever shows it.

What we observed: during a debugging session with a provider that intermittently returned empty completions (model streams reasoning, then ends the turn with zero text), two messages sent during that window each spawned a separate branch. Both branches ran concurrently in the same chat. One of them executed multi-file edits to a live file as its "reply"; that reply was never shown in the UI, and the assistant turn running in the other branch never saw the user message in its context. The split only became visible by auditing the database directly: both messages are stored as siblings with the same parent assistant message, each with their own assistant reply.

Why it happens: when a new message arrives, cptr decides between queuing it behind the running turn or starting a new branch, based on whether the parent message referenced by the request is a not-yet-done assistant message. That parent id comes from the frontend's currently rendered leaf. If the provider stream failed in a way that left the frontend's leaf stale (for example a turn that ends with no message content at all), the check concludes no turn is running, and the message becomes a new branch instead of being queued. There is no server-side check against the running-task state of the chat, only the done flag of one message.

Consequences: user messages can be answered by a branch invisible to the user; those answers execute tools against the workspace; the conversation silently diverges; and the visible assistant later answers without knowledge of messages the user believes it received. In a chat with tool access this is not just cosmetic: the hidden branch modifies files.

Suggested fix: when deciding queue-vs-branch, consult the server-side active task state of the chat (any running task, not just the done flag of the parent message). If any task is active, queue. Independent branches are a deliberate user action (regenerate, explore) and should stay possible, but they should never be created implicitly by a desynced leaf during a running turn.

Workaround for now: avoid sending messages while a turn shows no output for a long time; cancel the turn first, then send. Not a real fix, just damage control.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions