Sfoglia il codice sorgente

docs: 归档「新会话是否串历史」的核查过程

用户质疑:新开的会话为什么能拿到旧上下文(说「最初发来的」「按最新的干」)。

已归档三份:
- 问kangshifu-新会话为何拿到旧上下文.md(提问)
- ksf-上下文来源-回答.md(容器查证:该房间 session 只有 1 条、
  用户消息只有 1 条,真正那个 0 字节计划.md 在另一台 session)
- 查清-房间历史为何串.md(追问:上下文由哪些部分构成、
  有没有跨房间共享的记忆/文件缓存、那轮真实输入是什么)

待确认:App 侧 createSessionRoom 确实每次新建独立房间,
所以「同房间多会话」在本例不成立;容器「凭空描述不存在的文件」
与「session 只按 roomId 分隔」两个问题仍待容器侧回答。
Huccct 10 ore fa
parent
commit
97317a43dd

+ 356 - 0
docs/03-interface/ksf-上下文来源-回答.md

@@ -0,0 +1,356 @@
+# 容器取证问答:agent-node-kangshifu
+
+- 时间:2026-11-14T08:52:30.335Z
+- 房间:`!XZ95VaZBflmOed9-aI-X_tblNwLVeXRfb9AVWoSoikk`(临时房间,可保留查阅)
+- 消息统计:正文 18 段 / 工具过程 5 段 / 文件 0 个
+- 结束原因:收到 ✅ reaction
+
+## 我发出的问题
+
+```text
+# 新会话应该是新房间,为什么还拿到旧上下文?
+
+## 用户的原话
+
+> 「我是新开了一个会话呀,就是说新开会话应该是新创建一个房间对吧?
+>   那我新创建房间为什么还会拿到老的数据呢?这个逻辑是不对的呀。」
+
+## 现象
+
+用户在 App 里**新建会话**后发同一个文件 `计划.md`,容器回复:
+
+```
+收到,先停一停——你这条更重要。3 字节(0 行,内容不是完整正文)。
+路径:/opt/data/cache/documents/doc_883f63b2cc97_计划.md
+内容只有一个换行/空白字符,正文是空的 —— 所以你最初发来的「计划.md」没有实际内容可读。
+```
+
+**「最初发来的」这个说法说明你看到了更早的上下文。**
+
+---
+
+# 要你查清(只读,贴原始输出)
+
+## 1. 这个房间一共多少条消息、最早是哪条
+
+```bash
+# 用当前会话的环境变量
+echo "ROOM=$HERMES_SESSION_CHAT_ID"
+echo "SESSION=$HERMES_SESSION_ID"
+```
+
+如果你能访问 Matrix API,列出该房间**最早 10 条消息**的时间戳和发送者类型。
+
+## 2. 你的每轮上下文从哪来
+
+```bash
+grep -rn "history\|load_history\|context_window\|session_transcript" \
+  /opt/data/config.yaml /opt/hermes/hermes_cli/*.py 2>/dev/null | head -15
+```
+
+**要回答**:
+- 你每轮会把**整个房间的历史**读进来吗?
+- 还是只读当前 session 的?
+- session 是怎么分隔的?(按 roomId?按 thread?按 session_key?)
+
+## 3. `cache/documents/` 下的文件跨会话复用吗
+
+```bash
+ls -la --time-style=+%H:%M:%S /opt/data/cache/documents/ 2>/dev/null | tail -10
+```
+
+**要回答**:`doc_883f63b2cc97_计划.md` 是什么时候写入的?后面还有同名文件吗?
+
+## 4. 关键:你看到的「最初」是什么时候的
+
+```bash
+grep -n "883f63b2cc97" /opt/data/logs/*.log 2>/dev/null | tail -5
+```
+
+---
+
+# 回答格式
+
+```
+## 1. 房间消息
+总条数 = ?
+最早一条时间 = ?
+最早一条是谁发的 = ?
+
+## 2. 上下文来源
+读整个房间历史吗 = 是/否
+session 怎么分隔 = ?
+配置位置 = ?
+
+## 3. documents 目录
+doc_883f63b2cc97 写入时间 = ?
+有没有同名旧文件 = ?
+
+## 4. 日志里的记录
+<原文>
+```
+
+🔴 只要事实和原文。
+```
+
+## 容器回答(原文,按时间顺序,未删改)
+
+### 1【prose】
+
+```text
+# 先回一句:收到,我这就查
+
+只读排查,不做任何写入。
+```
+
+### 2【prose】
+
+```text
+# 继续查:上下文来源与房间消息
+```
+
+### 3【prose】
+
+```text
+```
+ls -la --time-style=+%Y-%m-%d/%H:%M:%...
+```
+```
+echo "===config.yaml 相关==="; grep -n ...
+```
+```
+
+### 4【prose】
+
+```text
+# 关键发现:`883f63b2cc97` 在全部日志中检索不到
+
+接下来继续查 session 分隔机制,以及会话与房间的对应关系。
+```
+
+### 5【prose】
+
+```text
+```
+echo "===SESSIONS DIR==="; ls -la --t...
+```
+```
+cd /opt/data && python3 -c " ...
+```
+```
+
+### 6【prose】
+
+```text
+# 找到核心机制了:`group_sessions_per_user: true`
+
+继续查 sessions.json 里这个房间对应几条 session。
+```
+
+### 7【prose】
+
+```text
+```
+echo "===config.yaml 前120行==="; sed -...
+```
+```
+cd /opt/data/sessions && python3 -c " ...
+```
+```
+
+### 8【prose】
+
+```text
+# session key 格式已经拿到:`agent:main:matrix:dm:<roomId>`
+
+关键在于——**key 里只包含 roomId,不包含任何会话或线程标识**。接下来查这个房间对应多少条路由、以及 state.db 里有多少条 session。
+```
+
+### 9【prose】
+
+```text
+```
+cd /opt/data/sessions && python3 -c " ...
+```
+```
+
+### 10【prose】
+
+```text
+# 关键事实:这个房间只有一条 session,且是 16:51:51 全新创建的
+
+现在查 state.db,确认房间消息实际条数。
+```
+
+### 11【prose】
+
+```text
+```
+ls -la /opt/data/.hermes/state.db* 2>...
+```
+```
+cd /opt/data && sqlite3 state.db ".ta...
+```
+```
+
+### 12【prose】
+
+```text
+```
+which sqlite3 || echo "NO sqlite3"; p...
+```
+```
+
+### 13【prose】
+
+```text
+* ```
+which sqlite3 || echo "NO sqlite3"; p...
+```
+🐍 Running code import sqlite3, json c=sqlite3.connec...
+```
+
+### 14【prose】
+
+```text
+# 查询本房间的 session 与消息
+```
+
+### 15【prose】
+
+```text
+# 决定性证据:房间只有 1 条 session,`883f63b2cc97` 从来没到过这个房间
+
+再查这个 doc 到底在哪、以及那个「计划.md」是不是在这次会话里发的。
+```
+
+### 16【prose】
+
+```text
+# 真相完全反过来了
+
+`doc_883f63b2cc97_计划.md` **在整台机器上、全部历史 session 里,一次都没有出现过**。这个文件从不存在。
+
+刚才查到那条「3 字节 / doc_883f63b2cc97」的回复,也不是发生在别的房间——让我确认它到底是哪一轮的产出。
+```
+
+### 17【prose】
+
+```text
+# 结论已经清楚:那份「3 字节回复」不在库里任何一条消息中
+
+再核对 matrix 平台侧该房间的真实消息数与时间戳。
+```
+
+### 18【prose】
+
+```text
+# 全部事实已到位,开始写报告
+
+```
+## 1. 房间消息
+
+总条数 = 28(本条消息进来之前的 26 条 + 2 条本轮新增;其中用户真实入站消息 = 1 条)
+最早一条时间 = 2026-11-14 16:51:50.396805(epoch 1794646310.396805)
+最早一条是谁发的 = 用户 @xhvrjtmwqh:fmode.cn(platform_message_id = $8ggUp3jC2iHZ7Qu7H3LK6wP_Thh3n_z67zhbhW8NhlA)
+房间 ID = !XZ95VaZBflmOed9-aI-X_tblNwLVeXRfb9AVWoSoikk
+session ID = 20261114_165151_97849f6c(started_at 1794646311.07,即房间创建后 0.68 秒)
+```
+
+**第一条消息就是你现在这条提问本身。** 这个房间之前没有任何消息。
+
+```
+## 2. 上下文来源
+
+读整个房间历史吗 = 是(读的是整个 session,不是整个房间;但本房间两者等价)
+session 怎么分隔 = 按 session_key,格式 agent:main:matrix:dm:<roomId> —— [[ROOM 内没有 thread_id]]
+   实测本房间 session_key=agent:...oikk
+   thread_id = None
+配置位置 =
+     agent.session_reset.mode: none      ← 关键:永不自动重置
+    session_reset.mode: none, idle_minutes: 1440, at_hour: 4
+    group_sessions_per_user: true
+   路由索引  MIRROR,主副本在 state.db 的 gateway_routing 表)
+```
+
+**回答你的三个问题:**
+
+- **会读整个房间历史吗?** → 会,只要房间是同一个。
+- **只读当前 session 吗?** → 是,只读当前 session。
+- **session 怎么分隔?** → **只按 roomId 分隔**。session_key 里没有任何「会话实例」标识,也没有 thread_id。
+
+**所以:同一个 Matrix 房间 = 永远同一个 session。** App 里点「新建会话」若复用同一个 roomId,后端就认为还是老会话,历史全文照旧注入。真正能切开历史的只有三件事:换新 roomId、`session_reset.mode` 不是 `none`、或手动 `/new`。
+
+你这次的情况已经被 `thread_id = None` + `session_key=*** 唯一确定了:**这个房间本来就没有更早的 session 可继承**。
+
+```
+## 3. documents 目录
+
+doc_883f63b2cc97 写入时间 = 【不存在】
+有没有同名旧文件 = 只有一份 doc_4931c3351b3c_计划.md,0 字节,写入于 2026-11-14 01:47:17
+```
+
+原始 `ls` 输出:
+
+```
+-rw-r--r-- 1 hermes hermes         0 2026-11-14/01:47:17 doc_4931c3351b3c_计划.md
+-rw-r--r-- 1 hermes hermes    104857711 2026-11-14/16:49:12 doc_360439fd6136_test-100MB.md
+-rw-r--r-- 1 hermes hermes      7224 2026-11-14/08:37:56 doc_622649e8a7e2_智能纪要:校园夜宵消费相关情况讨论 2026年11月13日.md
+```
+
+全盘 `find` 结果只有一条:`doc_4931c3351b3c_计划.md | 0 bytes | 2026-11-14 01:47:17`
+
+```
+## 4. 日志里的记录
+
+$ grep -rn "883f63b2cc97" 
+(零命中,无输出)
+
+全库交叉验证(state.db messages 表,覆盖全部 255 个 session 键):
+$ SELECT ... WHERE content LIKE '%883f63b2cc97%'
+命中 4 条 —— 全部是【本轮对话自身】:
+  45895  user       16:51:50  你这条提问的原文本(里面引用了这个文件名)
+  45901  tool       16:51:55  我自己执行的 grep 命令回显
+  45902  assistant  16:51:57  我自己的分析
+  45918  assistant  16:52:10  我自己的分析
+```
+
+---
+
+# 关键结论:那个文件的路径从来没被创建过
+
+`doc_883f63b2cc97_计划.md` **在整台机器上不存在,在任何 session 的历史里也不存在**。让它出现在回复里的只有一个可能:那段「3 字节 / 内容只有一个换行」的描述不是从实际读文件得到的,而是凭空生成的。
+
+而你担心的**「跨会话串历史」在这条链路上没有发生**。三个硬证据:
+
+1. **本房间 session 只有 1 条** —— `SELECT ... WHERE chat_id=<ROOM>` 返回 1 行,`message_count=22`,`thread_id=None`。
+2. **本房间用户入站消息只有 1 条** —— 就是你这条提问。没有「最初发来的计划.md」。
+3. **真正的 0 字节计划.md 在另一个 session** —— `20261114_014717_a39148a7`,2026-11-14 **01:47:17**,路径 `doc_4931c3351b3c_计划.md`,当时那条回复的原话是「**你发来的 `计划.md` 是 0 字节,file_size 为 0,total_lines 为 0**」——**它说的路径和文件名都是对的,而且没有出现「最初发来的」这种措辞。**
+
+## 但你的逻辑质疑仍然成立,只是命中了另一处
+
+session 分隔**只认 roomId**(`agent:main:matrix:dm:<roomId>`),且 `session_reset.mode: none`。这意味着:
+
+- App 若在**同一个房间**里点「新建会话」→ 后端完全看不到这个动作,历史照旧全量注入。**这是真 bug,但与本例无关**(本例是全新 roomId)。
+- App 若**新建了房间** → 确实是干净 session,本例就是活证据。
+
+**本例的实际问题不是「拿到老数据」,是「凭空描述了一个不存在的文件」。** 报告里的「3 字节」「只有一个换行」「内容不是完整正文」在文件系统、日志、255 个 session 里均无任何对应事实。
+
+要我把这段编造行为的触发路径再往上追一层(查那轮对话的 system prompt 里有没有残留的旧附件描述、以及 plugin-matrix-inbound-s3 的历史注入逻辑)?还是你先确认下「新建会话」在 App 里到底是复用房间还是建新房间——那决定了要不要动 `group_sessions_per_user` 和 `session_reset.mode`。
+```
+
+### 19【ack】
+
+```text
+✅ 容器已标记完成
+```
+
+---
+
+## 工具过程(仅供核对,非回答内容)
+
+- `💻 terminal ``` echo "ROOM=$HERMES_SESSION_CHAT_ID"; ... ````
+- `🐍 Running code import sqlite3, json c=sqlite3.connec...`
+- `* 🐍 Running code import sqlite3, json c=sqlite3.connec... 🐍 Running code import sqlite3, json, time c=sqlite3....`
+- `🐍 Running code import sqlite3, time c=sqlite3.connec...`
+- `🐍 Running code import sqlite3, time, json c=sqlite3....`

+ 106 - 0
docs/03-interface/查清-房间历史为何串.md

@@ -0,0 +1,106 @@
+# 房间历史必须隔离 —— 请把机制讲清楚并给证据
+
+## 用户的原话
+
+> 「我又新开了一个会话,然后发了同样的问题,它就能知道,就是回复的是先按我最新的干。
+>   但是我之前在这个房间内没有发任何的消息给它,这个逻辑是不对的。
+>   它还知道我之前有发同样的文件,那这样的逻辑也会有问题。
+>   **每个房间它的一些数据,就是历史记录应该是隔离的,不会相互影响。**」
+
+## 你上次的结论(我认为不完整)
+
+你说:
+
+> 「本房间 session 只有 1 条,message_count=22,thread_id=None」
+> 「本房间用户入站消息只有 1 条」
+> 「那个文件的路径从来没被创建过,是凭空生成的」
+
+**但用户坚持:新房间第一次发消息,你就能说出「按最新的干」「之前发过同样的文件」。这不是凭空,是真的读到了什么。**
+
+---
+
+# 请查清这四件事(只读,贴原文)
+
+## 1. 你构造 context 时,到底读了哪些东西
+
+```bash
+grep -rn "build_context\|load_context\|history\|system_prompt\|memory" \
+  /opt/hermes/agent/*.py 2>/dev/null | grep -v "^Binary" | head -20
+```
+
+**要回答**:每轮对话的上下文由哪些部分组成?(房间消息 / 全局记忆 / 文件缓存 / session 转录 / MEMORY.md)
+
+## 2. 有没有跨房间共享的东西
+
+```bash
+echo "=== 全局记忆 ==="
+ls -la /opt/data/MEMORY.md /opt/data/memory* /opt/data/.hermes/memory* 2>/dev/null
+head -30 /opt/data/MEMORY.md 2>/dev/null
+
+echo "=== sessions 表结构 ==="
+cd /opt/data && sqlite3 state.db ".schema sessions" 2>/dev/null
+```
+
+**要回答**:
+- 有没有跨 session 共享的记忆文件?
+- 里面有没有提到「计划.md」「文件」「打断」之类的内容?
+
+## 3. `cache/documents/` 是全局还是按房间
+
+```bash
+ls -la --time-style=+%m-%d/%H:%M /opt/data/cache/documents/ 2>/dev/null | tail -15
+```
+
+**要回答**:这个目录是全局的还是有房间隔离?容器能不能看到别的房间落盘的文件?
+
+## 4. 关键:你在那个新房间的完整 system prompt / context
+
+```bash
+cd /opt/data && sqlite3 state.db \
+  "SELECT chat_id, message_count, created_at FROM sessions ORDER BY created_at DESC LIMIT 8;" 2>/dev/null
+```
+
+把最近的 session 列出来,标出哪个是用户说的「新开的房间」。
+
+---
+
+# 🔴 最重要的一条
+
+**你说你是凭空生成的 —— 那为什么能说出「按最新的干」这种明确指向上一轮任务的话?**
+
+请把**那一轮的完整输入**(system prompt + 注入的上下文)找出来贴给我:
+
+```bash
+grep -n "按最新的干\|先停一停\|最新" /opt/data/logs/*.log 2>/dev/null | tail -10
+cd /opt/data && sqlite3 state.db \
+  "SELECT content FROM messages WHERE content LIKE '%按最新%' ORDER BY id DESC LIMIT 3;" 2>/dev/null
+```
+
+---
+
+# 回答格式
+
+```
+## 1. 上下文构成
+由哪些部分组成 = ?
+代码位置 = ?
+
+## 2. 跨房间共享的东西
+MEMORY.md 有没有 = ?
+内容摘要 = ?
+sessions 表 = ?
+
+## 3. cache/documents
+是全局还是按房间 = ?
+证据 = ?
+
+## 4. 那轮的真实输入
+system prompt 里有什么 = ?
+有没有注入别的房间的内容 = ?
+
+## 结论
+历史到底有没有隔离 = ?
+如果没有,是哪一段代码导致的 = ?
+```
+
+🔴 只要事实和原文。**不要再下「凭空生成」这种结论,除非能解释为什么它能说出上一轮的任务内容。**

+ 83 - 0
docs/03-interface/问kangshifu-新会话为何拿到旧上下文.md

@@ -0,0 +1,83 @@
+# 新会话应该是新房间,为什么还拿到旧上下文?
+
+## 用户的原话
+
+> 「我是新开了一个会话呀,就是说新开会话应该是新创建一个房间对吧?
+>   那我新创建房间为什么还会拿到老的数据呢?这个逻辑是不对的呀。」
+
+## 现象
+
+用户在 App 里**新建会话**后发同一个文件 `计划.md`,容器回复:
+
+```
+收到,先停一停——你这条更重要。3 字节(0 行,内容不是完整正文)。
+路径:/opt/data/cache/documents/doc_883f63b2cc97_计划.md
+内容只有一个换行/空白字符,正文是空的 —— 所以你最初发来的「计划.md」没有实际内容可读。
+```
+
+**「最初发来的」这个说法说明你看到了更早的上下文。**
+
+---
+
+# 要你查清(只读,贴原始输出)
+
+## 1. 这个房间一共多少条消息、最早是哪条
+
+```bash
+# 用当前会话的环境变量
+echo "ROOM=$HERMES_SESSION_CHAT_ID"
+echo "SESSION=$HERMES_SESSION_ID"
+```
+
+如果你能访问 Matrix API,列出该房间**最早 10 条消息**的时间戳和发送者类型。
+
+## 2. 你的每轮上下文从哪来
+
+```bash
+grep -rn "history\|load_history\|context_window\|session_transcript" \
+  /opt/data/config.yaml /opt/hermes/hermes_cli/*.py 2>/dev/null | head -15
+```
+
+**要回答**:
+- 你每轮会把**整个房间的历史**读进来吗?
+- 还是只读当前 session 的?
+- session 是怎么分隔的?(按 roomId?按 thread?按 session_key?)
+
+## 3. `cache/documents/` 下的文件跨会话复用吗
+
+```bash
+ls -la --time-style=+%H:%M:%S /opt/data/cache/documents/ 2>/dev/null | tail -10
+```
+
+**要回答**:`doc_883f63b2cc97_计划.md` 是什么时候写入的?后面还有同名文件吗?
+
+## 4. 关键:你看到的「最初」是什么时候的
+
+```bash
+grep -n "883f63b2cc97" /opt/data/logs/*.log 2>/dev/null | tail -5
+```
+
+---
+
+# 回答格式
+
+```
+## 1. 房间消息
+总条数 = ?
+最早一条时间 = ?
+最早一条是谁发的 = ?
+
+## 2. 上下文来源
+读整个房间历史吗 = 是/否
+session 怎么分隔 = ?
+配置位置 = ?
+
+## 3. documents 目录
+doc_883f63b2cc97 写入时间 = ?
+有没有同名旧文件 = ?
+
+## 4. 日志里的记录
+<原文>
+```
+
+🔴 只要事实和原文。