fix(title): stop model-switch marker from becoming the session title - #82207
fix(title): stop model-switch marker from becoming the session title#82207yy28 wants to merge 1 commit into
Conversation
Switching models before sending the first real message titled the session "[System: The active model for this chat has…" instead of the user's actual question. `_append_model_switch_marker` persists its notice with `role="user"` because strict OpenAI-compatible providers reject a system message that is not first (NousResearch#48338). Titling had no way to tell that apart from a genuine opening turn, which caused two distinct failures: 1. `_MACHINE_PREFIXES` did not cover the marker. Its `[System: ` prefix matches none of `[CONTEXT COMPACTION`, `[Runtime note:`, or `[SYSTEM]` (different case, no closing bracket), so `is_titleable_user_message()` returned True and the marker was formatted into the title. 2. `maybe_auto_title()` counted the marker as a user message. With the marker present, the first real question arrived at `user_msg_count == 2` and the `> 1` guard returned early, so the session was never titled at all and its `title` column stayed NULL. Fixing only (1) would therefore have traded a wrong title for a permanently missing one. Add the marker prefix to `_MACHINE_PREFIXES` (kept in sync with `tui_gateway.server._MODEL_SWITCH_MARKER_PREFIX`) and count only titleable user messages when detecting the opening turn. The guard stays narrow: ordinary user text that happens to start with "[System:" still titles normally. Adds 6 regression tests, verified to fail without the fix.
|
Superseded by #82390, which cherry-picks this commit so your authorship stays in the history. Your diagnosis was right and so was the shape of the fix — the narrow prefix rather than a broad What #82390 adds is the other half of the same bug. Filtering the count stops a marker from displacing the opening turn, but a session that merely opened with one is still nameless once it drifts past the threshold, and nothing reconsiders it (#76842). So the guard now needs both to agree: past the opening turn, and already named. The count is also taken over multimodal turns properly, so "here's a screenshot, fix the login" counts as the question it is. Thanks for the writeup on the issue — the repro and the root-cause trace are what made this quick to fold in. |
What does this PR do?
Switching the active model before sending the first real message in a new session titled the session
[System: The active model for this chat has…instead of the user's actual question._append_model_switch_marker(tui_gateway/server.py) persists its notice withrole="user"— deliberately, because strict OpenAI-compatible providers reject a system message that is not first (#48338). Titling had no way to tell that apart from a genuine opening turn, which caused two distinct failures:1.
_MACHINE_PREFIXESdid not cover the marker. The guard list was:The marker begins
[System: The active model for this chat has changed to, which matches none of the three ([SYSTEM]differs in case and has a closing bracket). Sois_titleable_user_message()returnedTrueand the marker was formatted straight into the title.2. The marker consumed the session's only titling opportunity.
maybe_auto_title()counted it as a user message, so the first real question arrived atuser_msg_count == 2and the> 1guard returned early — meaning no title was ever written andsessions.titlestayedNULL. The string users see in the sidebar is the UI falling back to rendering the first message.That second point is why fixing only the prefix list is not enough: it would trade a wrong title for a permanently missing one. Both are fixed here.
The fix is deliberately narrow — ordinary user text that happens to start with
"[System:"still titles normally.Related Issue
Fixes #82206
Type of Change
Changes Made
agent/title_generator.py"[System: The active model for this chat has changed to "to_MACHINE_PREFIXES, with a comment tying it totui_gateway.server._MODEL_SWITCH_MARKER_PREFIXso the two stay in sync.maybe_auto_title()now counts only titleable user messages when detecting the opening turn, so a machine-authored opener can no longer push the first real question out of the titling window.tests/agent/test_title_generator.pyTestModelSwitchMarkerNotTitleableclass (6 tests): prefix/constant sync, marker not titleable, unrelated[System:text still titleable, real question after a marker still titles, instant-title path skips the marker and uses the real message, plus one test documenting thatderive_titleis intentionally unguarded (the check lives in its callers).How to Test
Reproduce (before this patch):
/model custom:<provider>:<model>.hourly weather forecast for <city>.[System: The active model for this chat has…;sessions.titleinstate.dbisNULL.Verify the fix:
pytest tests/agent/test_title_generator.py -q # 24 passed pytest tests/tui_gateway/ tests/test_tui_gateway_server.py -qThen repeat steps 1–3: the session is titled from the real question.
The regression tests genuinely catch the bug. Removing just the new
_MACHINE_PREFIXESentry and re-running makes 4 of the 6 new tests fail:What platforms I tested on
macOS 26.6.1 (Apple Silicon), Python 3.11. The change is pure string/prefix logic with no OS-specific behavior, so Linux/WSL2 should be unaffected.
Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass — see note belowDocumentation & Housekeeping
docs/, docstrings) — inline comments explain the cross-module coupling; no user-facing docs affectedcli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/ANotes for reviewers
The coupling between
agent/title_generator.pyandtui_gateway.server._MODEL_SWITCH_MARKER_PREFIXis currently by convention: the prefix is duplicated as a literal because importingtui_gateway.serverfrom the titling path would be a heavier dependency than this fix warrants.test_marker_prefix_matches_gateway_constantasserts the two agree, so drift fails the suite rather than silently regressing.If you'd prefer a shared constant (e.g. hoisted into a small module both sides import), I'm happy to rework it that way.