diff --git a/backend/api/server.py b/backend/api/server.py
index 6a712cc..5c32ebe 100644
--- a/backend/api/server.py
+++ b/backend/api/server.py
@@ -1725,6 +1725,9 @@ def create_app(state: AgentState) -> FastAPI:
):
continue
title = session_metadata.get('title')
+ title_source = session_metadata.get('title_source')
+ if isinstance(title, str) and title_source != 'manual':
+ title = _clean_session_title(title)
if not isinstance(title, str) or not title.strip():
title = None
preview = ''
@@ -3917,6 +3920,8 @@ def _read_session_title(directory: Path, session_id: str | None) -> str | None:
return None
title = metadata.get('title')
if isinstance(title, str) and title.strip():
+ if metadata.get('title_source') != 'manual':
+ return _clean_session_title(title)
return title.strip()
return None
@@ -4044,8 +4049,30 @@ def _generate_session_title(
def _clean_session_title(value: str) -> str | None:
- title = ' '.join(value.strip().strip('"\'“”‘’`').split())
+ raw = value.strip()
+ if not raw:
+ return None
+ raw = re.sub(r'(?is).*?', '', raw)
+ raw = re.sub(r'(?is).*?', '', raw)
+ if re.search(r'(?i)<\s*(analysis|think)\b', raw):
+ return None
+ lines = [line.strip() for line in raw.replace('\r', '\n').split('\n')]
+ title = next((line for line in reversed(lines) if line), '')
+ title = re.sub(r'^\s*(?:[-*#>\d.、\s]+)', '', title)
+ title = re.sub(
+ r'^(?:会话标题|标题|短标题|生成标题|session title)\s*[::]\s*',
+ '',
+ title,
+ flags=re.IGNORECASE,
+ )
+ title = title.strip().strip('"\'“”‘’`*_ ')
+ title = re.sub(r'\*\*(.*?)\*\*', r'\1', title)
+ title = ' '.join(title.split())
title = title.rstrip('。.!!??')
+ if re.search(r'(?i)\b(generating|generate|thinking|reasoning)\b', title):
+ return None
+ if '<' in title or '>' in title:
+ return None
if not title:
return None
return title[:24].rstrip()
diff --git a/tests/test_gui_server.py b/tests/test_gui_server.py
index aadb983..11f512f 100644
--- a/tests/test_gui_server.py
+++ b/tests/test_gui_server.py
@@ -22,6 +22,7 @@ from backend.api.server import (
AgentState,
create_app,
_bash_bg_manager,
+ _clean_session_title,
_derive_initial_session_title,
_ensure_session_title,
_generate_session_title,
@@ -860,6 +861,35 @@ class GuiServerTests(unittest.TestCase):
self.assertIsNone(_read_session_title(directory, session_id))
+ def test_invalid_llm_title_is_not_preserved_as_previous_title(self) -> None:
+ with tempfile.TemporaryDirectory() as d:
+ root = Path(d)
+ _, state = _build_client(root)
+ session_id = 'invalid-llm-title'
+ directory = state.account_paths('alice')['sessions']
+ agent = state.agent_for('alice', session_id)
+
+ _save_in_progress_session(
+ directory=directory,
+ agent=agent,
+ session_id=session_id,
+ prompt='帮我生成一批地图导航测试数据。',
+ )
+ stored = load_agent_session(session_id, directory=directory)
+ save_agent_session(
+ replace(
+ stored,
+ session_metadata={
+ **(stored.session_metadata or {}),
+ 'title': '**Generating short title',
+ 'title_source': 'llm',
+ },
+ ),
+ directory=directory,
+ )
+
+ self.assertIsNone(_read_session_title(directory, session_id))
+
def test_session_title_refines_first_message_title_after_run(self) -> None:
with tempfile.TemporaryDirectory() as d:
root = Path(d)
@@ -960,6 +990,20 @@ class GuiServerTests(unittest.TestCase):
self.assertEqual(config.base_url, 'http://model.mify.ai.srv/v1')
client.complete.assert_called_once()
+ def test_clean_session_title_rejects_reasoning_leak(self) -> None:
+ self.assertIsNone(_clean_session_title('**Generating short title'))
+ self.assertIsNone(
+ _clean_session_title(
+ 'reasoning\nGenerating short Chinese title'
+ )
+ )
+
+ def test_clean_session_title_strips_markdown_and_prefix(self) -> None:
+ self.assertEqual(
+ _clean_session_title('**标题:地图导航测试数据。**'),
+ '地图导航测试数据',
+ )
+
def test_clear_runtime_state(self) -> None:
with tempfile.TemporaryDirectory() as d:
client, _ = _build_client(Path(d))