Эх сурвалжийг харах

docs: 按规范补写修改留痕与踩坑记录,更新文档索引

新增:
- docs/00-tracking/2026-11-08-修改留痕-会话隔离与容器状态统一.md
  (依 templates/change-record-template.md:改动内容/原因/影响/测试/文档同步/遗留)
- docs/00-tracking/2026-11-08-踩坑-会话隔离与状态判定.md
  (依 templates/lessons-learned-template.md:现象/根因/解决过程/经验总结/遗留)

记录两条重要经验:
- 数据「没到达」类问题,须先做服务端/内存/界面三方采样再定根因,
  禁止仅凭代码推断(本次 REQUIRE_MENTION 判断即因此误判并撤回)
- 时间点 ≠ 状态点:保护条件的解除应依据目标状态是否成立,而非某个时间节点

docs/README.md 补齐 00-tracking 索引(规则 11:新增文档必须同步索引)

Co-Authored-By: Claude Code <noreply@anthropic.com>
Huccct 2 цаг өмнө
parent
commit
00ce1cbf09

+ 126 - 0
docs/00-tracking/2026-11-08-修改留痕-会话隔离与容器状态统一.md

@@ -0,0 +1,126 @@
+# 修改留痕:会话隔离与容器状态统一(2026-11-08)
+
+| 字段 | 值 |
+|---|---|
+| 修改日期 | 2026-11-08 |
+| 关联需求 | 用户反馈(无 PRD 编号):新会话串历史 / 容器不回复 / 状态显示不实 |
+| 类型 | 缺陷修复 + 重构 |
+| 版本号 | v1.0 |
+| 提交 | `f6f6e48`(代码)、`30247be`(诊断记录) |
+
+---
+
+## 一、改动内容
+
+### 1. 会话隔离(新建会话不再带出历史)
+
+| # | 改动 | 文件 |
+|---|---|---|
+| 1 | `paginate()` 增加新建态保护 —— 它是三条历史回填路径中**唯一没有保护**的 | `core/chat.service.ts` |
+| 2 | `startBlank()` 清空 `_ownedThreadRoots`(该集合只在 `openReal` 重建,而 `startBlank` 不经过 `openReal`,残留上一会话的容器线程根) | `core/chat.service.ts` |
+| 3 | `hasMoreHistory` 在新建态恒为 `false`(`paginationToken()` 是**房间级** token,与 thread 无关) | `core/chat.service.ts` |
+| 4 | 两端首屏自动补齐加 `state === 'empty'` 短路 | `desktop/pages/chat/chat.ts`、`mobile/pages/chat/chat.ts` |
+| 5 | 新建态保护从「时间点解除」改为「状态点解除」—— 等本会话消息真落 timeline 才解除 | `core/chat.service.ts` |
+
+### 2. 容器状态全站统一
+
+此前项目里有 **4 套互不相通**的状态判定(裸 `status` / `agentPresence` / `resolveAgentState` / `TaskPulse.describe`),同一容器在不同页面结论相反。现全部收敛到 `resolveAgentState`(判心跳、防上报器自指)。
+
+| 位置 | 改动 |
+|---|---|
+| PC 新建会话弹窗 | **新增状态展示**(圆点 + 文案,按 tone 上色) |
+| PC 左栏会话行 | 新增状态点 |
+| 移动抽屉会话行 | 新增状态点 |
+| 移动通讯录 / 生命卡 / 聊天页副标题 / 状态抽屉 | 从裸 `status` 切到合成态;心跳由 ISO 原文改为「X 分钟前」 |
+| 两端工作台 | 「运行中 / 需要关注」计数与筛选改用合成态;移动端补上漏掉的 `stopped` |
+| `TaskPulseService.describe()` | 先判不可用;补 `isReporterSelfTalk` 过滤(原先把 `reporter heartbeat @ …` 当任务名显示) |
+| `isReporterSelfTalk()` | 加宽:`currentTask` 命中上报器文案也算自指 |
+
+### 3. 输出状态口径统一
+
+| 改动 |
+|---|
+| 静默窗口统一为 `QUIET_MS = 25s`(原回合卡 3s / 顶栏 20s,两处结论打架) |
+| 新增 `agentIdle` 判据:容器明确报 idle 时立即收尾,不必等窗口过期 |
+
+### 4. 归档回到原分组
+
+归档是「收起来」不是「搬走」。新增 `groupOf()` 作为唯一归属判据(群聊优先于工作区),PC 与移动同步。
+
+---
+
+## 二、改动原因
+
+| 问题 | 根因 |
+|---|---|
+| 新建会话仍显示旧内容 | 三条回填路径中 `paginate()` 无保护;且 `hasMoreHistory` 取自房间级 token,新建会话也能翻页 |
+| 容器状态不对 | 4 套判定并存;容器 reporter 把 `status=running` 硬编码 |
+| 「只闪一下正在回复就消失」 | 回合卡 3 秒窗口 vs 顶栏 20 秒窗口,容器分段输出间隔常超 3 秒 |
+
+---
+
+## 三、涉及文件与范围
+
+**前端(26 个文件)**:`core/chat.service.ts`、`core/chat-session.ts`、`core/chat-session.service.ts`、`core/agent-state.ts`、`core/assistant-turn.ts`、`core/turn-aggregator.ts`、`core/group-turns.ts`、`core/task-pulse.service.ts`、`core/mobile-chat-view.service.ts`、`core/group-store.service.ts`、`components/domain/session-tree/`、`components/domain/new-session-modal/`、`components/domain/assistant-turn/`、`desktop/pages/chat/`、`desktop/pages/workbench/`、`mobile/components/m-drawer/`、`mobile/components/m-life-card/`、`mobile/pages/chat/`、`mobile/pages/contacts/`、`mobile/pages/workbench/` + 4 个 spec
+
+- **后端**:无
+- **数据表/字段**:无
+- **接口变更**:无
+
+---
+
+## 四、影响范围
+
+- **受影响功能**:新建会话、消息回填、会话归档、容器状态展示(PC + 移动全站)
+- **破坏性变更**:无。数据格式不变,云端 `FmodeChatThread` 结构不变
+- **用户可见变化**:
+  - 新建会话不再出现「加载更早的消息」
+  - PC 新建弹窗、PC 左栏、移动抽屉多出状态点
+  - 状态文案统一(`running` 一律叫「正在执行」,「空闲」用中性灰不再用橙色)
+
+---
+
+## 五、测试情况
+
+| 项 | 结果 |
+|---|---|
+| `npx tsc --noEmit`(app + spec) | ✅ |
+| `npm test`(Karma + ChromeHeadless) | ✅ **1339 SUCCESS** |
+| `npx ng build` | ✅ |
+| 真实账号实测(PC) | ✅ 新建弹窗显示 `正在执行(绿) / 正在执行(绿) / 离线(红)`;左栏状态点同步 |
+| 容器 thread 隔离实测 | ✅ 自造 thread 根发问,容器回复挂同一根 |
+
+**新增测试**:
+- `chat-session.service.spec.ts`:空壳不因 `keepId` 复活 / 有内容会话仍被 `keepId` 保留
+- `chat.service.spec.ts`:发送请求未返回前,实时消息不得混入新会话
+- `chat-session.spec.ts`:`groupOf` / `groupKey` × 4
+- `task-pulse.service.spec.ts`:僵尸容器报离线 / 上报器自指不显示为任务名
+
+---
+
+## 六、文档同步情况
+
+- [x] `docs/00-tracking/操作追踪.md` 已追加
+- [x] 本修改留痕(本文)
+- [x] 踩坑记录:`docs/00-tracking/2026-11-08-踩坑-会话隔离与状态判定.md`
+- [ ] PRD / 技术方案 / ER 图 / API 文档 —— 本次未涉及
+- [ ] `docs/README.md` 索引 —— 待补
+
+---
+
+## 七、遗留事项
+
+**P0(明日处理)**
+- LoopGuard 误判:`repeated` 规则不要求连续、不区分会话,把正常回复挡在界面外
+- 新建会话内存残留多条「你好」(用户只发一条,`messages()` 返回 3 条)
+
+**P1**
+- 容器回复延迟约 13 秒,期间界面只有「正在思考…」
+- 容器 reporter `status=running` 硬编码,前端仅兜底
+
+**P2**
+- `belongsToSession()` 有重复代码(`:1670-1693` 与 `:1716-1739`)
+- `app-lj-test-v1`、`lj` 两个容器的配置未核查
+- 移动端缺少 LoopGuard 提示
+
+详见《踩坑记录》第五节。

+ 185 - 0
docs/00-tracking/2026-11-08-踩坑-会话隔离与状态判定.md

@@ -0,0 +1,185 @@
+# 踩坑记录:会话隔离与容器状态判定(2026-11-08)
+
+| 字段 | 值 |
+|---|---|
+| 日期 | 2026-11-08 |
+| 来源场景 | 用户真机 + 桌面实测:新建会话串历史 / 容器不回复 / 状态显示不实 |
+
+---
+
+## 一、现象
+
+用户报告三个问题,串起来看像「一个 bug」,实际是**三条独立链路**:
+
+1. **新建会话仍能看到历史消息** —— 新建后发「你好」,上方出现几天前的内容和「加载更早的消息」
+2. **发消息容器不回复** —— 界面上只有自己发的「你好」,没有 agent 回复,并出现「检测到自动对话循环,已暂停自动回复」
+3. **容器状态不对** —— 没对容器做任何操作,却显示「正在执行」
+
+---
+
+## 二、根因
+
+### 2.1 新建会话串历史:三条回填路径,只修了两条
+
+历史内容进入消息区有三条路径,新建态保护最初只加在前两条:
+
+| 路径 | 有保护? |
+|---|---|
+| `storeVersion` effect(/sync 落地后历史回填) | ✅ 已有 |
+| `appendIncoming`(实时事件追加) | ✅ 已有 |
+| **`paginate()`(向上翻页)** | ❌ **漏了** |
+
+而且 `paginate()` 的 `paginateBack()` 翻的是**整个房间**的 timeline,不是当前 thread 的 history。
+
+**触发链**:
+```
+新建会话 → 首屏只有 0~1 条消息、不满一屏
+  → 移动端 fillViewportIfNeeded() / 桌面端贴底逻辑自动 paginate()
+  → 拉回 30 条房间历史
+  → 按会话过滤时把旧内容认领进来
+```
+
+页面上还会出现「加载更早的消息」按钮,因为 `hasMoreHistory` 取自 `paginationToken()` —— **房间级** token,跟当前 thread 毫无关系。
+
+**第二个坑**:`_ownedThreadRoots` 只在 `openReal()` 里重建,而 `startBlank()` **不经过 `openReal`**,集合里残留着上一个会话的容器线程根。`paginate()` 恰好走这个集合。
+
+### 2.2 「容器不回复」:判断错了两次
+
+**第一次判断(错误,已撤回)**:认定 `MATRIX_REQUIRE_MENTION=true` 是根因。
+
+依据是文档里一句「私聊必须 false」。但直接问容器、看 `adapter.py` 源码后发现:
+
+```python
+if not thread_id:
+    if is_dm:
+        synthetic = (self._dm_mention_threads and is_mentioned) or self._dm_auto_thread
+```
+
+`REQUIRE_MENTION` 与 `is_dm` 是**两个分支**,私聊走 `_dm_mention_threads`,跟那个开关无关。
+
+**实际根因**:容器**有回复**,约 13 秒后到达 Matrix,thread 根完全正确。是 **LoopGuard 把回复挡在了界面外**。
+
+`loop-guard.ts:142-150` 的 `repeated` 规则:
+- **不要求连续**(不是「连续 3 条相同」)
+- **不区分会话**(在 A、B、C 三个会话各发一条「你好」也能凑够 3 条)
+- `dismissedFor` 按 **agentId** 记而非 sessionId → 切会话时暂停态残留
+
+### 2.3 容器状态:4 套判定并存
+
+| 体系 | 判定函数 | 用在哪 |
+|---|---|---|
+| A 运行态(原始) | `AGENT_STATUS_TEXT` / `agentStatusTone` / `AGENT_TONE` | 移动通讯录、生命卡、两端工作台 |
+| B 接入态 | `agentPresence()` | PC 通讯录 |
+| C 合成态 | `resolveAgentState()`(**判心跳**) | 只有移动会话列表 |
+| D 任务行 | `TaskPulseService.describe()` | PC 头部、右栏 |
+
+同一个僵尸容器(45 天没心跳、`status=running`):移动列表判红「离线」,PC 左栏显示绿「正在执行」。
+
+更深一层:容器 reporter 把 `status: "running"` **硬编码**(`操作追踪.md:909-914`),没任务时也照报。
+
+---
+
+## 三、解决过程
+
+| 步骤 | 做法 |
+|---|---|
+| 1 | 加运行时诊断(`__fmodeDebugOwner` 开关 + 归属判定现场日志) |
+| 2 | 读编译产物确认执行顺序,发现保护在「发送请求返回时」就解除了 |
+| 3 | 改为「等 timeline 里真出现本会话消息」才解除 |
+| 4 | 用真实账号 + 浏览器自动化,**每 3/8/15/25 秒三方采样**(Matrix / App 内存 / 界面) |
+| 5 | 采样结果显示:Matrix 上 13 秒时已有回复且 thread 正确;界面上 `loopPaused=true` |
+| 6 | 直接通过 Matrix 向容器发问(自造 thread 根,不污染会话),让它自查配置 |
+| 7 | 读 `adapter.py` 源码,确认 `REQUIRE_MENTION` 与 DM 无关,**撤回原判断** |
+
+---
+
+## 四、经验总结(可复用要点)
+
+### 4.1 判断「数据没到」之前,先确认数据到底在不在
+
+本次两次误判(犯同一个错):**基于代码推断就下结论,没去拿运行时证据**。
+
+正确姿势:写一个三方采样脚本,**同时**看「服务端有什么 / 前端内存有什么 / 界面显示什么」。三者一对比,是丢在传输、丢在过滤、还是丢在渲染,一目了然。
+
+### 4.2 时间点 ≠ 状态点
+
+「什么时候解除保护」这类设计,用**时间点**(发送返回后 / commit 后)永远会漏掉窗口期。应该用**状态**(目标条件是否真的成立)。
+
+本项目在这上面连续错了两次:
+- 第一次:`commit()` 时解除(本地记账,网络还没发)
+- 第二次:`sendReal()` 返回时解除(只是本机发出,服务端还没并入)
+
+最终改为:**timeline 里出现本会话消息**才解除。
+
+### 4.3 同一个语义只能有一个判定函数
+
+项目里 4 套状态判定并存,导致「同一容器在不同页面结论相反」。`resolveAgentState` 早就写好了(设计意图就是「唯一展示口径」),只是没人用。
+
+**规则**:数据的「呈现判定」必须收敛到单一纯函数,各页面只负责渲染。
+
+### 4.4 文档里的「配置建议」不等于「根因」
+
+`docs/03-interface/容器绑定App…20261103.md:169` 写着「私聊必须 false」,速查表也列着「发消息容器不回 ← REQUIRE_MENTION=true」。这两处都是**配置规范**,不是本次故障的解释。
+
+看到文档有对应条目时,还要**验证容器实际走的是不是那个分支**。本次差一点就直接按文档去重启容器了。
+
+### 4.5 让容器自查比自己猜快
+
+容器能读自己的 `.env`、`os.environ`、源码、进程状态。发一条结构化问题(要求「只读、不改配置、逐步回读证据」),它给的证据比前端推演戏可靠得多。
+
+本次就是靠它发现了 `container_environment` 里根本没有 `MATRIX_*`、值来自 `os.environ` + `adapter.__init__` 读一次。
+
+---
+
+## 五、遗留(明日处理)
+
+### P0
+
+| # | 问题 | 定位 |
+|---|---|---|
+| 1 | LoopGuard 误判,把正常回复挡在界面外 | `core/loop-guard.ts:142-150`、`core/loop-guard.service.ts:86-100` |
+| 2 | 新建会话内存残留多条「你好」(用户只发一条,`messages()` 返回 3 条) | 待采样 `_messages` 每次变化定位 |
+
+### P1
+
+| # | 问题 | 说明 |
+|---|---|---|
+| 3 | 回复延迟约 13 秒,期间只有「正在思考…」 | 缺「已送达,等待回复」类反馈 |
+| 4 | 容器 reporter `status=running` 硬编码 | 前端已兜底,根治需容器整改 |
+
+### P2
+
+| # | 问题 |
+|---|---|
+| 5 | `belongsToSession()` 有重复代码(`:1670-1693` 与 `:1716-1739`),第二段永远进不去 |
+| 6 | `app-lj-test-v1`、`lj` 两个容器配置未核查 |
+| 7 | 移动端缺少 LoopGuard 提示(PC 有,移动端没有任何提示) |
+
+### 容器配置现状
+
+| 项 | 值 | 说明 |
+|---|---|---|
+| `MATRIX_REQUIRE_MENTION` | 文件已改 `false`,**进程仍是 `true`** | 已证明与本问题无关;未重启 |
+| `MATRIX_IGNORE_USER_PATTERNS` | `@agent-node-lj-test:fmode\.cn` | 不含用户 mxId,正常 |
+| 值的注入方式 | `os.environ`,`adapter.__init__` 读一次 | 改文件不热生效 |
+| 容器内能否自助重启 | ❌ 无 `docker.sock`、无 s6 控制权 | 需宿主执行 |
+
+---
+
+## 六、是否已沉淀到规则
+
+- [ ] 已写入 claude-rules 规则
+- [x] 已写入项目 docs(本文件 + `2026-11-08-修改留痕-会话隔离与容器状态统一.md`)
+- [x] `操作追踪.md` 已追加
+
+**建议反哺规则**(待评审):
+> 规则 8 补充:数据「没到达」类问题,须先做**服务端 / 内存 / 界面三方采样**再下结论,禁止仅凭代码推断定根因。
+
+---
+
+## 七、关联
+
+- 修改留痕:`docs/00-tracking/2026-11-08-修改留痕-会话隔离与容器状态统一.md`
+- 操作追踪:`docs/00-tracking/操作追踪.md`(2026-11-08 各节)
+- 相关代码:`src/app/core/chat.service.ts`、`src/app/core/agent-state.ts`、`src/app/core/loop-guard.ts`、`src/app/core/assistant-turn.ts`
+- 相关文档:`docs/03-interface/容器会话与线程机制-20261028.md`、`docs/03-interface/会话存储与生命周期-20261101.md`

+ 2 - 0
docs/README.md

@@ -34,6 +34,8 @@
 | 文档 | 状态 | 说明 |
 |---|---|---|
 | [操作追踪.md](00-tracking/操作追踪.md) | 生效 | 🔴 综合日志,按动作逐条追加(⚠️ 12:43–13:46 段曾误删,已重建,见文首说明) |
+| [2026-11-08-修改留痕-会话隔离与容器状态统一.md](00-tracking/2026-11-08-修改留痕-会话隔离与容器状态统一.md) | 生效 | v1.0 · 会话隔离收尾 + 容器状态收敛到 `resolveAgentState` + 输出状态口径统一(提交 `f6f6e48`) |
+| [2026-11-08-踩坑-会话隔离与状态判定.md](00-tracking/2026-11-08-踩坑-会话隔离与状态判定.md) | 生效 | v1.0 · 两次误判的复盘 + 遗留 P0/P1/P2 清单(明日处理) |
 
 ### 01-requirements/ 需求文档
 | 文档 | 状态 | 说明 |