Repository navigation
Description
The gap
docs/architecture/chat.md lists this under Known gaps:
budget.pyprices the question at 1,024 tokens and saysMessageTextenforces that, butMessageTexthas no length limit, so a long question can push a turn past the model's window.
Why it matters
The whole budget is built on four parts adding up to the model's window: the system prompt, the excerpts, the question and the answer reserve. History is whatever is left. Three of the four are real caps. The question is not — MessageText sets a minimum length and nothing else, so a pasted page of text walks straight past its 1,024-token share.
What the user sees is not an error. llama.cpp stops generating wherever the window runs out, so a long question buys a reply that is cut off mid-sentence, or on a tight window no reply at all. The budget's own comment says this is handled at the wire, which is the kind of wrong that survives review because it reads as a fact.
Where
surfsense_local/backend/modules/chat/schemas.py—MessageTextis theStringConstraintsannotation to change.ThreadTitleright above it shows the house shape for a bounded string.surfsense_local/backend/modules/chat/budget.py—QUESTION_TOKENSand the comment claimingMessageTextenforces it. Whatever you decide, that comment has to end up true.surfsense_local/backend/modules/chat/history.py—_tokens()is the repo's existing characters-to-tokens estimate (len(text) // 4). Use it rather than inventing a second ratio.
What done looks like
- A message longer than the question's share is refused at the wire with a
422, before any model is resolved or any retrieval runs. - You pick the bound and you defend it in the PR. A character cap derived from 1,024 tokens is the obvious reading, but note the tension: the same cap applies to a remote model with a 128k window, where the question would have fit fine. Choosing one global cap over a window-aware one is a real decision, and a one-line comment saying which you chose and why is what the next reader needs.
- The frontend composer should not let someone type past the limit and only find out on send.
chat-composer.tsxis where that lives; if you add a message there, it is an interface string. - Delete the Known gaps line from
docs/architecture/chat.md.
What not to change
This gap is listed in two docs. docs/architecture/local-models/fit.md carries the same line under its own Known gaps, and its budget table describes the question's 1,024 as "A share, not a limit (see Known gaps)". Both that Known gaps line and that table note have to change in this PR too, or the docs will contradict each other the day it merges. Do not open a second issue for the fit.md line; it is this one.
How to verify
surfsense_local/backend/tests/unit/chat/test_budget.py holds the budget's unit tests and is where the "the question cannot exceed its share" test belongs. The 422 belongs in tests/integration/chat/test_chat.py. Write the failing test first.
cd surfsense_local/backend
uv run pytest -m unit
uv run pytest -m integration -k chat
uv run ruff check .
# from the repo root, because the PR edits two docs
cd ../.. && python scripts/check_docs.pyBefore you start
Read CONTRIBUTING.md, the chat doc and the budget section of the fit doc. If you add a composer message, interface strings in surfsense_local follow the translate skill described in AGENTS.md. Open your PR against dev, not main. Comment here to claim this.
Activity
ybai08 commentedon Sep 29, 2026
I'd like to take this one.
Plan: failing tests first, a
test_budget.pycase that the question cannot exceed its 1,024-token share and atest_chat.pycase that an oversized message is refused with422before any model or retrieval runs. Then boundMessageTextwith a character cap derived fromQUESTION_TOKENSthrough the existing_tokens()ratio, so the comment inbudget.pybecomes true, and mirror the limit inchat-composer.tsxso the composer says so before send. Both Known gaps lines (chat.md,fit.md) and the fit table note go in the same PR, againstdev.fix(local): cap a chat question at its share of the window
MODSetter commentedon Sep 29, 2026
Fixed by #2053,
fix(local): cap a chat question at its share of the window, by @ybai08. Merged intodev.Closing by hand rather than by keyword:
Fixes #1943only auto-closes on a merge to the default branch, and this repo merges throughdev(CONTRIBUTING), so this would have stayed open and claimable while the work was already done. It reachesmainwith the next release.Nothing left to pick up here.