Files
Agent-System/.doc/audit/2026-09-13-chat-history-refresh-disappears.md
T
weichenandCursor 6e744d3717 docs(test): record U10 ChatLog ports and live chat refresh evidence
Archive the finished chat mutex tasks, attach PI idle-chat refresh same-set evidence for project 6252, and stop expansion after in-place ports without a package split.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 04:11:40 +00:00

6.2 KiB
Raw Blame History

title, status, created, updated, severity
title status created updated severity
刷新后对话正文消失但统计仍存在 done 2026-09-13 2026-09-13 high

刷新后对话正文消失但统计仍存在

现象

用户在项目会话中可以正常与助手对话并生成文档;页面刷新后,消息正文区域显示为空,但顶部分类过滤和右侧分类锚点仍显示消息数量及首尾消息 ID。

结论:消息没有从数据库删除,而是在刷新后的服务端历史恢复链路中被阶段权限过滤器全部剔除。统计接口没有应用同一权限范围,因此仍返回未过滤的数量和锚点。

根因分析

1. 刷新前与刷新后使用不同消息来源

  • 刷新前,web/src/App.tsx:1634-1656 中的组件内 messages 保存本轮实时消息,useChatWindow 将它们合并到画面。
  • 刷新后,组件内 messages 从空数组重新开始,正文依赖 GET /api/v1/chat/messages/window 恢复。
  • web/src/hooks/useChatWindow.ts:112-120 只要服务端返回非空 days,就用返回桶覆盖窗口和本地缓存;即使桶中的 messages 已被过滤为空也照常覆盖。

因此,实时画面正常不代表历史恢复链路正常。

2. 文档任务消息没有绑定工作流阶段

  • 普通聊天路径会在 internal/chat/session.go:267-274,425-438 把 ActiveStageKey 写入用户与助手消息的 metadata.stage_key。
  • 文档生成使用的异步任务路径在 internal/server/task_run_handlers.go:81-96 只写入 project_id、client_turn_id、task_key、task_status,没有写 stage_key、workflow_instance_id 或 stage_instance_id。
  • 任务完成投影 internal/server/task_finalize.go:58-85 仍未补这些作用域字段。

这使得文档任务在当前页面可见,但重载后无法通过阶段可见性判定。

3. 工作流激活前的历史也会被追溯性隐藏

  • internal/server/stage_access_helpers.go:135-163 在项目进入受限工作流后,明确丢弃所有 metadata.stage_key 为空的历史消息。
  • internal/projectinstance/stage_access.go:15-77 根据当前 live 实例计算可见阶段;策略一旦进入 Restricted=true,激活前产生、天然没有阶段键的消息也无法恢复。
  • StageKeyAllowed("") 本身在 internal/projectinstance/stage_access.go:103-108 返回允许,但外层过滤器在调用它之前已经对空阶段执行 continue;现有单测只验证前者,没有覆盖组合行为。

这不是合理的“拒绝越权”,而是缺少对 pre_activation/legacy 对话作用域的正式建模。

4. 统计与正文的授权口径分裂

  • 窗口和按日正文在 internal/server/chat_window_api.go:48-50,104-106 应用阶段过滤。
  • 分类与日期统计在同文件 111-155 直接查询并返回原始统计,没有会话所有权校验,也没有阶段过滤。
  • internal/ctxmgr/chat_window.go:622-749 的 SQL 对会话内全部未删除消息聚合,返回数量及 MIN/MAX(id)。
  • 前端 CategoryFilterBar 使用这些服务端统计显示数量;CategoryDock 还以统计中的首尾 ID 作为锚点兜底。因此正文为空时,数量与锚点仍存在。

此外,所有这些端点虽由全局中间件要求登录,但窗口、日期、分类等端点没有统一调用 assertSessionOwnedByUser。这会造成私聊会话对象级授权不一致,并可能泄露其他会话的消息存在性、分类数量和消息 ID。

运行态证据(dev,2026-09-13)

只读查询 shuimu-context-pg-dev/agent_system_dev:

项目/会话 结果
project 143 创建者 user 9,live instance 89,active stage V0.1_triage
session sess-p143-u9 共 4 条未删除消息,4 条正文长度均大于 0
激活前消息 431-432 创建于 instance 89 之前,无 stage_key
文档任务消息 433-434 创建于激活之后,task_key=intake,仍无 stage_key
过滤结果 4 条均命中“空 stage_key → 丢弃”,窗口日桶保留但消息数组为空
分类统计 仍按全量 4 条聚合,可返回数量及首尾 ID

全库当前有 live 工作流实例的会话消息统计也显示:所有命中的消息均缺少 stage_key。这说明不是单条偶发数据,而是写入链路缺字段与历史兼容策略共同造成的系统性问题。

严重度

high,并应作为发布 blocker:

  • 核心对话记录在刷新/重新登录后不可用,用户会误认为数据丢失;
  • 文档任务的对话留痕无法恢复,破坏可审计性;
  • 正文、搜索、日期、统计、锚点的授权集合不一致;
  • 分类数量和消息 ID 可在对象级授权不完整时形成侧信道泄露。

关联整改

证据

  • web/src/App.tsx:1634-1656,3393-3506
  • web/src/hooks/useChatWindow.ts:81-170,446-578
  • web/src/api/chatWindow.ts:73-164
  • internal/chat/session.go:267-280,425-468
  • internal/server/task_run_handlers.go:76-108
  • internal/server/task_finalize.go:58-96
  • internal/server/stage_access_helpers.go:135-212
  • internal/server/chat_window_api.go:12-155
  • internal/ctxmgr/chat_window.go:39-174,622-749
  • dev PostgreSQL 只读查询(未读取或落盘消息正文)
  • 相关测试通过但缺场景:go test -short ./internal/server -run 'TestServer_ChatWindow' -count=1 -v、go test -short ./internal/ctxmgr -run 'TestChatWindow' -count=1 -v、cd web && npm test

开放问题

  • 激活前项目私聊应默认仅对会话所有者可见,还是按项目成员和退出留痕策略共享,需要在实现前固化为策略测试;本方案建议保持现有“每人独立私聊”语义,优先按会话所有者授权。
  • 历史上若同一 task_key 在多个工作流实例/阶段重复出现,不能仅按当前模板名称盲目回填,必须结合运行实例、创建时间和不可变实例 ID;无法唯一归属的数据应标记为 legacy_unscoped 并走显式兼容策略。