fix(title): stop model-switch marker from becoming the session title

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
(#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.
This commit is contained in:
yy28
2026-08-09 11:23:41 +08:00
committed by Brooklyn Nicholson
parent 53a4003208
commit b684cbb094
2 changed files with 104 additions and 1 deletions

View File

@@ -132,6 +132,15 @@ _MACHINE_PREFIXES = (
"[Runtime note:",
"[System note:",
"[SYSTEM]",
# Model-switch marker from tui_gateway.server._append_model_switch_marker.
# It is persisted with role="user" (strict OpenAI-compatible providers
# reject a system message that is not first — #48338), so without this
# entry it looks like a real opening turn: switching models before the
# first real message titled the session
# "[System: The active model for this chat has…" instead of the user's
# actual question. Keep in sync with
# tui_gateway.server._MODEL_SWITCH_MARKER_PREFIX.
"[System: The active model for this chat has changed to ",
)
@@ -673,10 +682,19 @@ def maybe_auto_title(
# turn prologue, and after it when called post-response, so accept both.
# Entries are dicts; anything else means a caller passed the wrong
# positional and titling must degrade quietly rather than raise.
#
# Machine-authored openers are excluded from the count. They are persisted
# with role="user" (see _MACHINE_PREFIXES), so counting them would make a
# session that opened with e.g. a model-switch marker look like it was
# already past its opening turn — the real first question then arrives at
# count 2 and never gets titled at all, leaving the session permanently
# NULL-titled.
user_msg_count = sum(
1
for m in (conversation_history or [])
if isinstance(m, dict) and m.get("role") == "user"
if isinstance(m, dict)
and m.get("role") == "user"
and is_titleable_user_message(m.get("content") or "")
)
if user_msg_count > 1 and not _session_is_untitled(session_db, session_id):
return