Repository navigation
a </think> reply still names a conversation: the control-token title guard misses the think family #1337
Description
What happened
2026-09-21, PR #1071 at 78ddda475 (on dev@49b4a58fb), codeaf against a throwaway home with deepseek/deepseek-v4-pro-0813 through OpenRouter. The naming call for a new conversation came back as a provider control token and it became the conversation's title on the tab, on Home and in the Sessions tab:
<|DSML|notice>
The fix commit on that PR added titleHasControlTokens in internal/session/title.go, which now rejects that family (<|, <|, [im_start], [im_end]) and repairs a stored one through healedTitle. It does not reject </think> or <think>, which contain neither marker. Those are the tokens this project has actually been bitten by before (the reasoning-tag loops behind #308 and the reply guard), from the same providers, so a naming reply of </think> or </think> fix the login bug will still be drawn as a title on every surface.
Replication
Deterministic (no model).
titleHasControlTokens("</think>") returns false today, and cleanTitle("</think> fix the login bug") returns a title that starts with the tag. A table case beside the existing control-token cases in internal/session/title_test.go shows it without a model. Through the real door: the tmux e2e suite's stub model (internal/e2e) answering the naming request with </think> names the conversation </think> on the tab row.
Field (real models).
Any conversation named by a DeepSeek or Qwen model on a day the provider leaks its reasoning close tag; it fires on the naming call, not on the reply, so it costs one short call. No control run needed: the deterministic case is the evidence.
Where
titleHasControlTokens in internal/session/title.go, and its two callers cleanTitle and healedTitle in the same file.
The fix
The guard also rejects <think> and </think> (case-insensitive, with or without surrounding text), on the same terms as the <| family: the reply is unusable, the conversation keeps its prompt label until naming produces a clean phrase, and a stored title carrying one is repaired on the next read. Legitimate titles that merely contain angle brackets (compare <div> and <span> rendering, fix the <T> generic bound) keep passing.
Acceptance
- e2e: through the naming road with a stub model whose title reply is
</think> fix the login bug: the tab row and the Home row show the prompt label, and the captured frame contains no</think>. - e2e: control: a stub reply of
compare <div> and <span> renderingis drawn verbatim on the tab row. - Unit:
titleHasControlTokensis true for</think>,<think>,<|im_start|>and<|DSML|notice>, false for<div>;healedTitleof a stored</think>returns the empty title so the caller falls back to the prompt label. - The manual page that covers conversation names says a reply that is a provider control token is not used as a name, and the change entry's
invalidatesnames what people believed before.
Activity
santoshkumarradha commentedon Sep 21, 2026
Classification:
bug,area:session,sev:papercut→ milestone Reliable agent.area:session— the title is minted in the session's naming seam (internal/session/title.go); the guard that should catch the control token lives there, not on any surface.sev:papercut— the wrong name shows on the tab, Home and the Sessions tab; nothing is lost or misrouted, it costs recognition.drafted with CodeAF