Skip to content

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

Closed
yy28 wants to merge 1 commit into
NousResearch:mainfrom
yy28:fix/model-switch-marker-session-title
Closed

fix(title): stop model-switch marker from becoming the session title#82207
yy28 wants to merge 1 commit into
NousResearch:mainfrom
yy28:fix/model-switch-marker-session-title

Conversation

@yy28

@yy28 yy28 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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 with role="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_PREFIXES did not cover the marker. The guard list was:

_MACHINE_PREFIXES = ("[CONTEXT COMPACTION", "[Runtime note:", "[SYSTEM]")

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). So is_titleable_user_message() returned True and 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 at user_msg_count == 2 and the > 1 guard returned early — meaning no title was ever written and sessions.title stayed NULL. 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

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • agent/title_generator.py
    • Added "[System: The active model for this chat has changed to " to _MACHINE_PREFIXES, with a comment tying it to tui_gateway.server._MODEL_SWITCH_MARKER_PREFIX so 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.py
    • New TestModelSwitchMarkerNotTitleable class (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 that derive_title is intentionally unguarded (the check lives in its callers).

How to Test

Reproduce (before this patch):

  1. Start a new session.
  2. Switch the model before sending anything, e.g. /model custom:<provider>:<model>.
  3. Send a normal first question, e.g. hourly weather forecast for <city>.
  4. Sidebar shows [System: The active model for this chat has…; sessions.title in state.db is NULL.

Verify the fix:

pytest tests/agent/test_title_generator.py -q          # 24 passed
pytest tests/tui_gateway/ tests/test_tui_gateway_server.py -q

Then repeat steps 1–3: the session is titled from the real question.

The regression tests genuinely catch the bug. Removing just the new _MACHINE_PREFIXES entry and re-running makes 4 of the 6 new tests fail:

FAILED test_marker_prefix_matches_gateway_constant
FAILED test_marker_is_not_titleable
FAILED test_real_question_after_marker_still_titles
FAILED test_instant_title_skips_marker_uses_real_message

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

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass — see note below
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.6.1 (Apple Silicon), Python 3.11

Note on the full suite: tests/agent/test_title_generator.py (24) and tests/tui_gateway/ pass. In my local checkout two pre-existing failures remain in test_tui_gateway_server.py (test_load_enabled_toolsets_rejects_disabled_mcp_env, test_load_enabled_toolsets_falls_back_when_tui_env_invalid) — both read local config and expect the default toolset list, and my config enables an extra toolset. They are unrelated to titling and fail independently of this patch.

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — inline comments explain the cross-module coupling; no user-facing docs affected
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) — N/A, pure string logic
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Notes for reviewers

The coupling between agent/title_generator.py and tui_gateway.server._MODEL_SWITCH_MARKER_PREFIX is currently by convention: the prefix is duplicated as a literal because importing tui_gateway.server from the titling path would be a heavier dependency than this fix warrants. test_marker_prefix_matches_gateway_constant asserts 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.

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.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 9, 2026
@OutThisLife

Copy link
Copy Markdown
Collaborator

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 [System: match, which your own test_unrelated_system_bracket_text_still_titleable shows would have swallowed a real question. I'd written the broad version before finding this PR and dropped it for yours. The sync guard against _MODEL_SWITCH_MARKER_PREFIX is kept too.

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.

@OutThisLife OutThisLife closed this Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Model switch before first message titles session "[System: The active model..." (and leaves title NULL)

3 participants