From 752c3a663cdf83aca33cd7021ed7f99cbfb448ae Mon Sep 17 00:00:00 2001 From: jasmine889966 <31839662+jasmine889966@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:10:31 +0800 Subject: [PATCH] fix(server): auto-title in the session's language, not the prompt's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The titling instruction asked for a '4-5 word title' in English without pinning an output language, so non-English sessions (zh/ja/ko/…) got English titles. Tell the model to write the title in the same language as the user's opening message, and treat the word count as an English-scale hint only — the natural equivalent brevity elsewhere (CJK titles are a few characters, not 4-5 words). The small-talk sentinel stays the fixed English token it always was (normalization already tolerates casing/punctuation riffs). Tests: the title call's system prompt pins the same-language rule; a CJK opener keeps a CJK title end to end (sanitizer whitespace/quote handling and the 80-char cap are script-agnostic); the sentinel still fires for a non-English small-talk opener and earns its single retry. --- coworker/server/manager.py | 17 ++++++++++++----- tests/test_autotitle.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/coworker/server/manager.py b/coworker/server/manager.py index cf9b740fa..266b0f8c3 100644 --- a/coworker/server/manager.py +++ b/coworker/server/manager.py @@ -5211,13 +5211,20 @@ def _extra_roots_of( ] # -- LLM auto-titles (FB-010) ------------------------------------------------- + # The title must speak the user's language: with no language pinned, an English + # instruction ("4-5 word title") yields English titles for Chinese/Japanese/… + # sessions. Same-language also relaxes the word count to a length that is natural + # for the opener's script (CJK titles are a few characters, not 4-5 "words"). _AUTOTITLE_PROMPT = ( "You title chat sessions. Given the user's opening message(s) — and, when " - "present, the assistant's first reply for context — reply with ONLY a 4-5 word " - "title for the session, named after what the session is actually about — no " - "quotes or punctuation wrapping it. If there is no topic at all (" - '"hey", "how are you", "hi there" and a generic reply), reply with exactly: ' - "small-talk" + "present, the assistant's first reply for context — reply with ONLY a short " + "title for the session, named after what the session is actually about. " + "Write the title in the same language as the user's opening message " + "(4-5 words for English; the natural equivalent brevity for other " + "languages) — no quotes or punctuation wrapping it. If there is no topic " + "at all (" + '"hey", "how are you", "hi there" and a generic reply), reply with ' + "exactly: small-talk" ) def _maybe_autotitle(self, session_id: str) -> None: diff --git a/tests/test_autotitle.py b/tests/test_autotitle.py index a6929ae6a..f722c2b71 100644 --- a/tests/test_autotitle.py +++ b/tests/test_autotitle.py @@ -173,3 +173,38 @@ async def go(): assert mgr._autotitle_attempts[sid] == 1 asyncio.run(go()) + + +async def test_title_call_pins_the_opener_language(tmp_path): + # With no language pinned in the titling instruction, an English system prompt + # ("4-5 word title") biases the model toward English titles even for non-English + # sessions. The prompt must tell the model to match the opener's language. + mgr, provider = _mgr(tmp_path, [_text("好的")], ["日本行程规划"]) + await _turn(mgr, "lang1", "帮我规划一下日本的旅行") + + system = provider.title_calls[0][0]["content"] + assert "same language as the user's opening message" in system + + +async def test_non_english_opener_keeps_non_english_title(tmp_path): + # End to end: a CJK title survives the sanitizer (whitespace collapse and quote + # strip don't touch CJK) and the 80-char absurdity cap, and is stored verbatim. + mgr, _ = _mgr(tmp_path, [_text("好的,我来帮你安排")], ["日本行程规划"]) + await _turn(mgr, "lang2", "帮我规划一下日本的旅行") + assert mgr.session_store.load("lang2").title == "日本行程规划" + + +async def test_non_english_small_talk_still_hits_the_sentinel(tmp_path): + # The sentinel is a fixed English token regardless of the session's language; + # normalization must keep catching it after the prompt rewording. + mgr, provider = _mgr( + tmp_path, + [_text("你好呀"), _text("在的")], + ["small-talk", "季度汇报整理"], + ) + await _turn(mgr, "lang3", "你好") + assert mgr.session_store.title_state("lang3")["auto_title"] is None + + await _turn(mgr, "lang3", "帮我整理季度汇报") + assert len(provider.title_calls) == 2 + assert mgr.session_store.load("lang3").title == "季度汇报整理"