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

8.5 KiB
Raw Blame History

完整历史保存与分页契约

1. 范围与触发

会话保存、读取、原始日志补齐、导入、运行结束及重启恢复涉及本契约。成功保存的正文与工具数据必须能从 cc-web 自身读取;预览大小限制只用于快照缓存和实时工具预览,不能限制完整历史及完成消息的阅读。

2. 函数签名

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. 错误与正确示例

// 错误:把预览当成唯一提交,完整正文在写档案前已经裁剪。
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同时检查完整读取与有界实时预览。