# 完整历史保存与分页契约 ## 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/.json` 是持久提交点,格式为 `{ version: 1, revision, complete, resetRevision, session }`。`session` 包含完整可序列化字段,排除运行态句柄;正文、工具、数组和深度不使用预览限额。 - `.json` 是有界快照缓存。`historyArchiveRevision` 位于顶层首字段,用来判断缓存是否过期;不得从嵌套同名键推断修订。 - `CC_WEB_SESSION_PERSIST_MAX_MESSAGES`、`CC_WEB_SESSION_MESSAGE_CONTENT_MAX_CHARS` 等预览配置不能限制完整档案;真实完成和恢复路径也不得先裁剪消息再提交。 - 可信来源保存在存储实例的 `WeakMap`,不能凭 JSON 的修订字段伪造。当前修订精确保存删除、清空和插入;旧修订根据加载基线合并互不冲突的修改,保持活跃消息及工具对象引用。 - 归档提交顺序为临时文件写入、文件 `fsync`、原子 `rename`、推进基线,再更新快照。档案失败不得更新快照;档案已提交而快照失败仍返回成功。 - 列表枚举快照与档案 ID 的并集。首次快照失败后,孤立档案仍须可读、可列出;缓存继续失败时使用内存有界预览。 - Codex App 运行态写入 `_history/_turns/.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`同时检查完整读取与有界实时预览。