Session 自动重命名
CC 用 Haiku 在首条用户消息上一次性生成英文标题(统一模块 sessionTitle.ts,多 surface 复用);Codex CLI 零生成、title=首条用户消息原文——harness 必须自建本地化命名管线
Session 自动重命名
结论
自动命名是「runtime 能力参差、harness 必须补齐」的典型案例,且补齐的具体姿势两边不同。Claude Code 有完整的 AI 标题生成器:单一模块 utils/sessionTitle.ts 调 Haiku、固定英文 prompt(sentence-case 3-7 词、JSON schema 输出),触发点是首条真实用户消息、一次性;但在 2026-03-31 快照的交互模式里生成结果只喂终端 tab 标题,落盘成 ai-title 条目的是 SDK/IDE/远程会话路径。Codex CLI 则完全没有生成逻辑(全仓 grep 为负),ThreadMetadata.title 就是首条用户消息原文。结论:在 CC 上 harness 的问题是「英文标题需要本地化覆盖 + 防回写竞态」,在 Codex 上是「从零自建」。Yoda 的做法是两边统一自建管线:spawn 用户已装的 agent CLI 生成标题(复用其认证),用 titleSource 三态保证自家标题不被 runtime 的自动名覆盖。
研究问题
- CC 的标题生成用什么模型、什么 prompt、何时触发、写到哪里?
- 生成的标题能否被外部覆盖而不被回写冲掉?
- Codex 侧「看起来像标题」的字段到底是什么?
- harness 如何在不引入独立 API client 的前提下做本地化标题生成?
各 Agent 设计与实现
Claude Code
生成器:单一模块 + Haiku + 英文硬编码 prompt。 [一手源码] utils/sessionTitle.ts:1-13 的头注释自述这是「全 surface 的 AI 标题单一事实源」(此前 teleport.tsx 和 /rename 各有一个 Haiku 生成器,保留作兼容)。核心调用(sessionTitle.ts:79-112):
const result = await queryHaiku({
systemPrompt: asSystemPrompt([SESSION_TITLE_PROMPT]),
userPrompt: trimmed,
outputFormat: { type: 'json_schema', schema: { ... title: string ... } },
options: { querySource: 'generate_session_title', ... },
})prompt(sessionTitle.ts:56-68)要求 "concise, sentence-case title (3-7 words)",给的全部是英文正反例({"title": "Fix login button on mobile"}),没有任何 locale 指令——这就是「CC 自动标题永远是英文」的根源 [一手源码]。长对话输入会被尾切到最近 1000 字符(MAX_CONVERSATION_TEXT,sessionTitle.ts:26-54)。失败静默返回 null,打 tengu_session_title_generated 遥测。
交互模式触发:首条真实用户消息,一次性,resume 不重titled。 [一手源码] screens/REPL.tsx:2684-2697:找第一条非 meta 用户消息,跳过斜杠命令/<bash-input> 等合成消息(「等真正的 prose」),haikuTitleAttemptedRef 保证只尝试一次;该 ref 在有 initialMessages 时初始化为 true(REPL.tsx:1133)——resume 的会话不会被从对话中段重新起名。注意快照中的关键事实:交互路径的结果只进 terminalTitle = sessionTitle ?? agentTitle ?? haikuTitle ?? 'Claude Code'(REPL.tsx:1135),即终端 tab 标题,不落盘。
落盘路径(ai-title 条目)在 SDK/远程侧。 [一手源码]:
- SDK 控制请求
generate_session_title(cli/print.ts:3783-3805):fire-and-forget 调 Haiku,persist: true时saveAiGeneratedTitle写ai-title条目;注释明确建议有 stale-write guard 的客户端(VS Code)传persist: false、走自己的 rename 路径以避免「AI 标题在用户中途改名之后才落地」的竞态(sessionStorage.ts:2658-2666)。 - CCR 远程会话(
hooks/useRemoteSession.ts:515-530):首条消息生成标题,失败 fallback 为消息截断 75 宽。 - Bridge headless(
bridge/initReplBridge.ts:302-345):消息数 1 时先用deriveTitle占位 + Haiku 升级,消息数 3 时对全量对话重新生成;用genSeq序号防止 count-1 的 Haiku 晚于 count-3 返回而覆盖更优标题,且每次落地前重查/rename——用户显式改名永远短路自动名。
[推断] Yoda 的 CC watcher 在线上版本(2.1.x)的交互会话 transcript 里观测到了 ai-title 行(yoda/src/main/core/session-title/claude-title-source.ts:18-21 注释),说明 03-31 之后的版本已把交互模式的 Haiku 标题也持久化了——快照滞后约两个月,此处行为已演进。
防覆盖设计回顾(详见《Session 命名同步》):ai-title 与 custom-title 是不同条目类型 + 不同字段名,读取偏好 customTitle > aiTitle,reAppendSessionMetadata 永不重追加 AI 标题(sessionStorage.ts:2641-2666)——AI 标题被设计为「可再生的临时物」,外部覆盖它是安全且预期的。
另有 /rename 的辅助生成器 generateSessionName(commands/rename/generateSessionName.ts:10-45):同样 Haiku,但 prompt 要求 kebab-case 2-4 词("fix-login-bug" 风格),产物走 custom-title(视为用户改名)。
Codex CLI
没有任何自动命名。 [一手源码] 在 core/ app-server/ tui/ cli/ 全量 grep generate.*title|title.*generat|summariz.*title 为负。ThreadMetadata.title 的注释就叫 "A best-effort thread title"(state/src/model/thread_metadata.rs:90-91),其唯一赋值点是 rollout 回放时取首条用户消息原文(state/src/extract.rs:97-102):
if metadata.title.is_empty() {
let title = strip_user_message_prefix(user.message.as_str());
if !title.is_empty() { metadata.title = title.to_string(); }
}显式命名只有 thread/name/set API(外部调用者负责生成内容);prefer_existing_explicit_title(thread_metadata.rs:242-255)在 backfill 对账时区分「显式标题」与「等于首条消息的自动标题」,保护前者。[推断] Codex App(桌面端)的自动重命名由 App 自己生成后经 API 写入——CLI/core 仓库中无此逻辑,无法源码验证 App 侧实现。
边界与相邻机制
- 用户可关闭标题反映:
settings.terminalTitleFromRename !== false控制/rename结果是否进终端标题(REPL.tsx:1127-1128);titleDisabled时整条 Haiku 生成路径不触发(REPL.tsx:2684)。 task-summary是另一条"自动描述"管线,别与标题混淆:CC 每 min(5 步, 2 分钟) fork 主线程生成「agent 正在干什么」的滚动摘要,写task-summary条目供claude ps展示(types/logs.ts:87-98注释);它不被 re-append、读最新一条即可,语义是状态而非命名(sessionStorage.ts:2676-2682)。harness 做会话列表的「副标题/进行中描述」应读它而不是 aiTitle。- 空输入防御:
generateSessionTitle对空描述直接返回 null(sessionTitle.ts:83-84);REPL 失败时清 ref 允许下条消息重试(REPL.tsx:2694-2696)。
差异矩阵
| 维度 | Claude Code | Codex CLI |
|---|---|---|
| 自动命名 | 有:Haiku + 固定英文 prompt(sentence-case 3-7 词) | 无:title = 首条用户消息原文(extract.rs:97-102) |
| 模型/调用 | queryHaiku + JSON schema 强约束输出 | — |
| 触发时机 | 首条真实用户消息一次性;bridge 模式 count-3 再升级一次 | — |
| 语言 | 英文硬编码,无 locale 感知 | 跟随用户输入语言(因为就是原文) |
| 持久化 | ai-title transcript 条目(SDK/远程路径;快照中交互路径仅终端标题) | SQLite threads.title / session_index.jsonl(仅显式 set) |
| 外部覆盖安全性 | 安全:AI 标题可再生、永不 re-append、CAS 只保护用户标题 | 安全:显式 set 直接覆盖;注意 backfill 会保护「显式标题」 |
| harness 策略 | 本地化标题覆盖(写 custom-title 或自有 DB) | 全量自建生成 |
最小复现
# CC:headless 下触发标题生成并观察落盘(SDK stream-json 模式发 generate_session_title 控制请求后)
grep '"type":"ai-title"' ~/.claude/projects/<slug>/<session-id>.jsonl
# 观察英文-only:任意中文首条消息的会话,ai-title 仍是英文
# Codex:确认 title 即首条消息原文(第三列为 1 = 自动标题,0 = 显式命名过)
sqlite3 ~/.codex/state_5.sqlite "SELECT title, first_user_message, title = first_user_message FROM threads ORDER BY created_at DESC LIMIT 3;"
# Codex:确认 CLI 仓库无生成逻辑(应为空输出)
grep -rni "generate.*title\|title.*generat" --include="*.rs" \
codex-rs/core/src codex-rs/app-server/src codex-rs/tui/src codex-rs/cli/src | grep -v testHarness 接入建议(Yoda 实践)
Yoda 对两个 runtime 统一采用自建命名管线 + 来源防覆盖:
- 生成不走独立 API client,而是 spawn 用户已装的 agent CLI(
generateConversationTitle.ts→task-naming-service的resolveNamingRuntime/requestAgentNamingPayload;摘要同理走runAgentCli,generateSessionSummary.ts:11)。好处:复用用户的认证与模型配置,零额外密钥;代价:延迟高于直连 API,需做超时(Yoda 摘要 90s 超时,SUMMARY_TIMEOUT_MS)。 - 本地化在自己手里:prompt 上下文取最近 12 条消息 / 6000 字符,标题截到 48 字符(
generateConversationTitle.ts:37-40);摘要管线有显式语言档位'app' | 'prompt' | 'en' | 'zh-CN'(generateSessionSummary.ts:38)——这正是对 CC 英文-only 的覆盖方案。 - 防回写竞态用来源三态而非时序:
titleSource ∈ {user, yoda, agent},watcher 把 runtime 自动名只在titleSource === 'agent'(或空)时落库(session-title-manager.ts:69-73)。比「在 CC 自动命名完成之后再覆盖」的时序方案更稳——不依赖对 runtime 内部时序的假设,CC 的ai-title哪怕在 Yoda 命名之后才到也写不进来。 - 命名 runtime 与会话 runtime 解耦(
generateSessionSummary.ts:27-31注释):标题/摘要用独立配置的 utility agent 跑,会话本身的 runtime 挂了(如认证过期)命名功能仍可用。
失效条件
- CC 若给
SESSION_TITLE_PROMPT增加 locale 感知(跟随用户输入语言),harness 覆盖管线可降级为兜底; - CC 交互模式持久化行为已在快照后变化(
[推断]项),新快照到手需确认ai-title的交互写入点; - CC 更换标题模型(Haiku → 其他)或改触发策略(如 bridge 的 count-3 升级推广到交互模式);
- Codex CLI 若引入自动命名(向 App 看齐),Yoda 的「
agent来源仅 interim」规则仍成立,但需确认其写入位置(SQLite vs API 通知)。
参考资料
- CC 源码:
utils/sessionTitle.ts(生成器全文)、screens/REPL.tsx:1129-1135, 2684-2697、cli/print.ts:3783-3805、bridge/initReplBridge.ts:302-345、hooks/useRemoteSession.ts:515-530、commands/rename/generateSessionName.ts - Codex 源码:
state/src/extract.rs:84-102、state/src/model/thread_metadata.rs:90-91, 242-255 - Yoda 实现:
src/main/core/conversations/generateConversationTitle.ts、generateSessionSummary.ts、src/main/core/session-title/session-title-manager.ts - 姊妹章:《Session 命名同步》(存储与冲突收敛细节)