Files
cc-web/.trellis/spec/backend/session-history.md
shiyue 69b6db0f5b 修复完整会话历史与长气泡交互并更新 CentOS 7 发布包
提交当前工作区全部源码、回归脚本、任务记录及重新构建的 Bun baseline 发布包。发布单文件 MCP 初始化及前端资源一致性验证通过。
2026-10-10 23:40:16 +08:00

101 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 完整历史保存与分页契约
## 1. 范围与触发
会话保存、读取、原始日志补齐、导入、运行结束及重启恢复涉及本契约。成功保存的正文与工具数据必须能从 cc-web 自身读取;预览大小限制只用于快照缓存和实时工具预览,不能限制完整历史及完成消息的阅读。
## 2. 函数签名
```js
createSessionHistoryStore({ sessionsDir, fs? })
store.load(id) // 完整会话或 null;注册可信加载基线
store.save(session) // { revision, complete, session, rebased };失败抛错
store.markLegacy(session) // 注册旧快照来源与完整性状态
store.markComplete(session, { reset? }) // 确认可靠补齐或主动清空;返回状态还原函数
store.state(session) // 来源、修订与完整性;不暴露消息基线
store.listIds()
store.revision(id)
store.remove(id)
store.saveTurn(id, state) // 完整 Codex App 运行态先原子提交
store.loadTurn(id) // 优先于有界运行预览
store.listTurnIds()
store.removeTurn(id) // 完整消息已提交或主动清空后才清理
saveSession(session) // 完整档案提交成功为 true,否则 false
loadSession(id) // 优先完整档案;无档案才读取旧快照
resolveSessionHistory(session) // { messages, source, available, recoverable }
```
## 3. 存储与响应契约
- `${SESSIONS_DIR}/_history/<id>.json` 是持久提交点,格式为 `{ version: 1, revision, complete, resetRevision, session }`。`session` 包含完整可序列化字段,排除运行态句柄;正文、工具、数组和深度不使用预览限额。
- `<id>.json` 是有界快照缓存。`historyArchiveRevision` 位于顶层首字段,用来判断缓存是否过期;不得从嵌套同名键推断修订。
- `CC_WEB_SESSION_PERSIST_MAX_MESSAGES`、`CC_WEB_SESSION_MESSAGE_CONTENT_MAX_CHARS` 等预览配置不能限制完整档案;真实完成和恢复路径也不得先裁剪消息再提交。
- 可信来源保存在存储实例的 `WeakMap`,不能凭 JSON 的修订字段伪造。当前修订精确保存删除、清空和插入;旧修订根据加载基线合并互不冲突的修改,保持活跃消息及工具对象引用。
- 归档提交顺序为临时文件写入、文件 `fsync`、原子 `rename`、推进基线,再更新快照。档案失败不得更新快照;档案已提交而快照失败仍返回成功。
- 列表枚举快照与档案 ID 的并集。首次快照失败后,孤立档案仍须可读、可列出;缓存继续失败时使用内存有界预览。
- Codex App 运行态写入 `_history/_turns/<id>.json`,包含完整正文、工具及映射;有界运行态文件只作预览。重启枚举完整运行态,恢复消息提交成功后才清理,异常档案保留证据。
- 用户提问必须保存成功后才启动 runtime 或接受 steer;完成、重启恢复及跨会话返回必须检查保存结果,失败保留可重试内容。CLI 恢复标识须区分回合,同一已提交回合不可因清理失败再次追加。
- 确认原始边界后的旧前缀必须先进入自身档案。`complete=false` 保留旧缺口状态,不能因创建档案就宣称旧历史完整。
- 响应的 `historyCursor` 是尚未展示的已有消息数;正数允许分页。`historyAvailable=false` 只说明旧来源待核实,不能阻断正文或用户目录。完整档案响应 `historySource='archive'`、`historyAvailable=true`。
## 4. 校验与错误矩阵
| 条件 | 行为 |
|---|---|
| 非字母、数字、下划线或连字符 ID | `history_invalid_id`,不写入 |
| 已有档案但传入未可信加载的短预览 | `history_untrusted_source`,保留已有档案 |
| 档案 JSON 或结构异常 | 读取失败,禁止回退短快照覆盖档案 |
| 旧修订缺可靠消息 ID | `history_stale_unidentified`,不覆盖新记录 |
| 旧清空请求遇到后来追加 | `history_stale_clear`,要求重新加载 |
| 清空已提交后旧对象再保存 | 允许无冲突元数据更新;禁止旧历史及线程复活 |
| 会话已删除或档案已移除 | `history_removed`,旧对象不能重新创建 |
| 完整档案写入失败 | `saveSession=false`,保留上次提交;调用方不得宣称保存成功并清理唯一未提交输出 |
| 完整档案成功、快照失败 | `saveSession=true`,记录缓存错误,重开从档案读取 |
| 原始日志缺失或边界未确认 | 仅返回自身已有消息,保留待核实状态,不猜测补入 |
## 5. 正常、基本与失败场景
- 正常:650 条及长正文、125 条工具调用保存后,快照仍有界;存储重开和原始日志移开后,完整内容相等。
- 基本:短会话重复保存后正文、顺序和稳定 ID 不变;主动删除、清空、撤回不会复活旧消息。
- 失败:磁盘写入失败只保留上次提交及可重试的未提交内容;旧短预览不能作为新完整历史覆盖已有档案。
## 6. 必需回归与断言
- `scripts/session-history-store-unit.js`:实际生产保存和读取入口、超限内容、存储重开、两类写失败、孤立档案、并发及活跃引用、删除/清空/撤回、确认前缀先归档。
- 真实完成与恢复路径使用超限正文和工具,断言归档原样读回;不能仅绕过生产入库函数直接构造消息数组。
- 真实 runtime 事件覆盖纯工具、纯文本与结构块混合、Claude user/tool_result;恢复覆盖图片内容块、提交失败保留以及重复恢复幂等性。任务看板的克隆适配器需重新可信加载并只更新元数据。
- `scripts/session-history-unit.js` 与 `scripts/history-frontend-unit.js`:57 条、首屏 12 条、剩余 45 条在来源待核实状态下仍全部可读,顺序正确、无重复,用户目录完成索引。
- 隔离浏览器:确认旧前缀后移开夹具原始日志,重启测试服务、重新登录,再从 `archive` 分页到最早消息;继续运行、刷新及重连保持内容。
- 单测设置 60 秒上限;真实会话和生产服务不能代替隔离夹具。
## 7. 错误与正确示例
```js
// 错误:把预览当成唯一提交,完整正文在写档案前已经裁剪。
session.messages.push({ content: truncateTextValue(entry.fullText, previewLimit) });
writeFileAtomicSync(sessionPath(session.id), buildSessionJsonForPersist(session).json);
// 正确:保存完整值,归档先提交,再生成有界预览缓存。
session.messages.push({ content: entry.fullText });
const committed = store.save(session);
try {
const preview = buildSessionJsonForPersist({ ...committed.session, historyArchiveRevision: committed.revision });
writeFileAtomicSync(sessionPath(session.id), preview.json);
} catch (error) {
// 完整档案已提交,缓存失败不改成历史保存失败。
plog('WARN', 'session_snapshot_cache_save_failed', { error: error.message });
}
```
实际代码还须处理稳定 ID、可信加载、运行态排除和快照写失败;这些保护由统一保存入口承担。
## 8. 同一任务的自动续跑与运行中重连
- 自动延续重试交接完整 `fullText`、全部工具、消息及工具去重映射、稳定助手消息ID和逻辑回合标识。模型提示可只保留末尾7000字,气泡与归档不能使用该提示替代完整输出。
- 清理中断轮前先提交完整运行态;重试等待期仍须被认定为运行中,并可通过 `resume_generating` 读回完整状态。完整保存失败须保留唯一输出,不能继续破坏性交接。
- 重试使用独立 `runtimeItemScope`,原生item ID跨轮复用不能覆盖较早输出;高频长正文增量可分片发送,拼接结果必须等于原始增量。
- 正常新用户任务从空正文开始。耗尽、取消、启动失败及最终保存失败均须保留已产生正文,并防止旧回合晚到通知结束新回合。
- `turn/start`已发送且原生回合ID未知时取消,旧轮须继续持有线程门禁。通知先到也归旧轮;确认身份并精确中断后才能放行新任务,RPC超时不能凭空释放身份。
- 完整运行态、`done.assistantMessage`和历史分页应提供全部正文及工具输入/结果;不能让有界工具预览在完成或刷新时覆盖完整气泡。
- 必需回归:`codexapp-retry-history-unit.js`覆盖A/B/C三次续跑、等待期重连、全部工具、存储重开、耗尽、取消、启动交错、保存失败、重启恢复及正常新轮隔离;`history-runtime-unit.js`同时检查完整读取与有界实时预览。