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>
6.2 KiB
6.2 KiB
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 可在对象级授权不完整时形成侧信道泄露。
关联整改
- Remediation: ../remediation/2026-09-13-chat-history-refresh-remediation.md
- Short-term: ../archive/ops/2026-09-13-chat-history-refresh-remediation.md
- Gap matrix: ../remediation/gap-matrix.md
证据
web/src/App.tsx:1634-1656,3393-3506web/src/hooks/useChatWindow.ts:81-170,446-578web/src/api/chatWindow.ts:73-164internal/chat/session.go:267-280,425-468internal/server/task_run_handlers.go:76-108internal/server/task_finalize.go:58-96internal/server/stage_access_helpers.go:135-212internal/server/chat_window_api.go:12-155internal/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并走显式兼容策略。