Skip to content

fix(local): discard a chat turn that ends with no answer text - #2037

Merged
MODSetter merged 1 commit into
MODSetter:devfrom
Cedric921:fix/chat-empty-reply
Sep 29, 2026
Merged

MODSetter merged 1 commit into
MODSetter:devfrom
Cedric921:fix/chat-empty-reply

Conversation

@Cedric921

@Cedric921 Cedric921 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

What

A chat turn that ends with no answer text is discarded, both halves and the deferred rename, whether generation failed or closed cleanly. The discard guard in stream() goes from failed and not parts to not parts, in the finally block and in the early return, so both still agree.

Why

A stream can close cleanly with nothing to say; the usual way on the local runtime is a thinking model whose trace spends the whole answer cap. The turn was kept, leaving a blank assistant bubble, and on a first turn the thread was renamed after a question nothing answered.

Decisions, as the issue asks:

  • What the client is told. The clean case sends error with kind unknown, then [DONE], the same frame order as a failed turn. Deleting the turn also removes the person's own message, so closing in silence would make it vanish unexplained. I reused unknown rather than adding a kind: its text ("Something went wrong generating a reply. Try again.") is accurate, a retry can succeed since sampling differs between runs, and a new kind would need a translated string in every locale. empty_reply_error() in errors.py names that choice in one place.
  • A reply that reasoned but never answered is discarded too. A trace with no answer is not a reply the thread can use, and the doc already says a failed turn is dropped "even if the model had reasoned"; this keeps the two cases the same.
  • Unchanged: a turn that fails midway keeps its partial text; only the failed and half of the guard changed.

docs/architecture/chat.md: the stream prose now covers the clean case, and the Known gaps line is removed.

Fixes #1942

How to test

cd surfsense_local/backend
uv run scripts/fetch_embedding_model.py
uv run pytest tests/integration/chat tests/unit/chat   # 48 passed
uv run ruff check modules/chat tests/integration/chat
cd ../.. && python scripts/check_docs.py

test_a_reply_with_no_text_leaves_no_trace is parametrized over no reasoning and reasoning-only; both failed before the change. The stub llama-server gains set_answer() so a test can make it answer with no deltas.

High-level PR Summary

This PR fixes an issue where chat turns that end with no answer text (e.g., when a thinking model exhausts its answer capacity during trace generation) were incorrectly kept in the chat history, resulting in blank assistant bubbles and inappropriate thread renames. The fix ensures these empty turns are discarded regardless of whether generation failed or closed cleanly, by updating the discard guard from failed and not parts to not parts. Clean closures now send an unknown error kind before the [DONE] frame to explain why the user's message was removed.

⏱️ Estimated Review Time: 5-15 minutes

💡 Review Order Suggestion
Order File Path
1 docs/architecture/chat.md
2 surfsense_local/backend/modules/chat/errors.py
3 surfsense_local/backend/modules/chat/router.py
4 surfsense_local/backend/tests/integration/chat/conftest.py
5 surfsense_local/backend/tests/integration/chat/test_chat.py

Need help? Join our Discord

A stream that closed cleanly with no text, as a thinking model does when
its trace spends the whole answer cap, kept a blank assistant turn and
renamed the thread. The discard guard was 'failed and not parts'; it is
now 'not parts', and the clean case sends an unknown error so the client
knows the question was removed with it.

Fixes MODSetter#1942
@vercel

vercel Bot commented Sep 29, 2026

Copy link
Copy Markdown

@Cedric921 is attempting to deploy a commit to the Rohan Verma's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 3267d62e-9c68-4836-8cf5-5507ec20bdfe

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@MODSetter
MODSetter merged commit 219a7b6 into MODSetter:dev Sep 29, 2026
16 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants