# 标题历史定位时间线 ## 目标 当 LLM 通过 `ccweb_set_title` 修改会话标题时,持久化标题变更事件,并在“定位”列表中按对话顺序显示不可点击的标题分隔项;同时按消息时间插入精确到天的日期分隔项,让用户看出主题在哪一步变化以及对话发生在哪一天。 ## 阶段 | 阶段 | 状态 | 验收标准 | |---|---|---| | 1. 建立计划并确认需求边界 | complete | 计划、发现、进度和 TODO CSV 已建立,且不覆盖现有任务文件 | | 2. 定位标题 MCP、消息存储与定位弹层链路 | complete | 找到精确入口、数据流、现有测试和兼容边界 | | 3. 补充标题历史及日期定位展示回归 | complete | 新断言覆盖持久化、接口返回、只读展示、历史兼容及浏览器本地 23:59/00:01 跨天边界 | | 4. 实现标题变更事件持久化与读取 | complete | 成功改名时写入事件,忽略/失败不写,旧数据可读取 | | 5. 实现定位列表标题与日期时间线展示 | complete | 标题项按步骤排序;日期按天去重并呈现横线中置日期气泡;两类节点均不可点击 | | 6. 运行专项与全量验证 | complete | 语法、专项回归、全量回归和 diff 检查通过 | | 7. 审查改动并清理临时跟踪文件 | complete | 代码审查通过,TODO CSV 删除,计划与进度收口 | ## 关键约束 - 只记录由标题 MCP 实际生效的变更;用户手动锁定导致的 ignored 不产生伪历史。 - 标题历史是时间线元数据,不伪装成普通对话消息,不参与消息定位选中。 - 日期节点只显示 `YYYY-MM-DD`,按浏览器当前用户本地时区分组,与现有消息时间的前端展示语义一致,不显示时分秒。 - 日期节点采用两侧横线、中间日期气泡的分隔样式;本地时间从 23:59 跨到 00:01 时必须产生新日期节点。 - 兼容已有会话数据;历史中没有标题事件时维持当前行为。 - 不覆盖工作区内与本任务无关的未提交文件和规划状态。 - 测试命令最长 60 秒;未确认无其他 running 会话前不重启服务。 ## 错误记录 | 错误 | 尝试 | 处理 | |---|---|---| | 标题历史专项回归因缺少 `normalizeTitleHistory` 失败 | 1 | 失败符合 TDD 预期,进入最小后端实现 | | 后端实现后专项回归因缺少前端 `normalizeOutlineTitleHistory` 失败 | 1 | 后端纯函数契约已通过,进入前端实现 | | `ccweb_list_conversations(status=running)` 60 秒超时 | 1 | 降级检查 PM2、端口、近期会话文件;发现另一条 15:38 收到用户消息的 Codex App 会话,按约束暂缓重启 | | `codebase-memory-mcp detect_changes` 返回 transport closed | 1 | 不重复同一失败,使用已完成的函数级检索、全量回归与本地 diff 审查收口 | | 首轮标题前置计划审查要求细化锚点与传输契约 | 1 | 补充字段名、索引基准、合法范围、优先级、降级规则,以及实时/重载/缓存回归要求后重新审查 | | 标题前置专项回归在后端锚点保留断言失败 | 1 | 符合 TDD 预期,确认现有 `normalizeTitleHistory()` 会丢弃新字段,进入最小后端实现 | | 后端实现后专项回归在标题前置顺序断言失败 | 1 | 后端锚点契约已通过,失败符合预期,进入前端锚点排序实现 | | 最终 `codebase-memory-mcp detect_changes` 返回 transport closed | 1 | 不重复失败调用,改用函数级 diff、专项回归、全量回归与 `git diff --check` 完成影响面审查 | | 首次全量回归调用未保留最终退出码 | 1 | 确认残留测试进程已结束后,改用可追踪 session id 重新运行并取得 exit 0 与通过标记 | ## 追加迭代:标题前置锚点(2026-07-29) | 阶段 | 状态 | 验收标准 | |---|---|---| | 1. 梳理现有标题事件与消息索引契约 | complete | 明确现有标题为何排在触发消息之后,并确定不破坏历史数据的锚点字段 | | 2. 补充标题前置及旧历史兼容回归 | complete | 新标题事件和无锚点旧事件均断言为“日期 → 标题 → 用户消息” | | 3. 持久化标题事件的触发消息锚点 | complete | LLM 实际改名事件记录 `anchorMessageIndex`;实时事件、会话重载、前端归一化与 snapshot 缓存均保留该字段,非法数据被清洗 | | 4. 按锚点重排定位列表展示顺序 | complete | 标题显示在所属用户消息之前,不可点击且不占消息编号;锚点缺失或不可见时按既定回退规则处理 | | 5. 运行语法检查与专项回归 | complete | server/app/regression 语法检查及标题时间线专项回归通过 | | 6. 运行全量回归并审查最终差异 | complete | 全量回归、diff check 和影响面审查通过,临时 TODO CSV 已清理 | ### 追加约束 - `messageIndex` 继续表示标题事件实际发生时的消息位置,不复用或改写其历史语义。 - 新增可选字段 `anchorMessageIndex`:零基整数,指向改标题时 `session.messages` 中最近一条 `role === 'user'` 的消息;合法值必须 `>= 0` 且 `< messageIndex`,否则归一化时丢弃该字段但保留合法标题事件。 - `anchorMessageIndex` 与 `messageIndex` 同时存在时,展示归属优先使用锚点;精确锚点消息未出现在当前定位消息集时,回退到 `messageIndex` 之前最近的可见用户消息;仍无候选时保留事件自身的时间位置。 - 旧历史没有 `anchorMessageIndex` 时,从定位列表中选择 `messageIndex` 之前最近的用户消息作为兼容锚点,不改写磁盘历史。 - 标题归属到触发消息后,日期也采用该用户消息的本地日期,保证固定顺序为“日期 → 标题 → 用户消息”。 - 锚点字段必须贯穿 MCP 返回和持久化、实时 `session_renamed.titleEvent`、重新加载的 `session_info.titleHistory`、前端 `normalizeOutlineTitleHistory()` 与 session snapshot 缓存;实时打开定位列表和重新进入会话都应得到相同顺序。 - 回归至少覆盖:新事件即时归属、重新加载后锚点仍存在、旧事件无锚点回退、跨午夜仍按锚点消息日期分组。 - 不修改 `.planning/.active_plan` 指向的其他在途任务,不触碰其 TODO CSV。