12 KiB
12 KiB
完整历史保存与分页契约
1. 范围与触发
会话保存、读取、原始日志补齐、导入、运行结束及重启恢复涉及本契约。成功保存的正文与工具数据必须能从 cc-web 自身读取;预览大小限制只用于快照缓存和实时工具预览,不能限制完整历史及完成消息的阅读。
2. 函数签名
createSessionHistoryStore({ sessionsDir, fs? })
store.load(id) // 完整会话或 null;注册可信加载基线
store.loadTail(id, count = 180, { maxBytes? }) // 只加载可修改尾部,保留全局索引
store.loadMetadata(id) // 只加载元数据,保存时保留全部消息
store.loadWindow(id, start, end) // 可修改中间窗口,保存时保留前缀和后缀
store.loadHistoryPage(id, before, count = 24) // { messages, total, start, end, complete, revision }
store.findMessage(id, predicate) // 按块查找,内存只保留少量块
store.forEachMessage(id, callback) // 按块遍历,用于附件清理等全历史操作
store.messageIndex(id, messageId) // 从轻量清单查询全局消息索引
store.countMessages(id, role) // 从清单计数,不加载正文
store.save(session, { taskBoardMetadata? }) // { revision, complete, session, rebased, taskTrackingPreserved };失败抛错
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, options?) // 默认尾部最多180条、2MiB;单条超大消息完整保留
// options 支持 metadata、count、window、messageId、promptId;显式 full 才全量加载
resolveSessionHistory(session, { count?, before? }) // 有界出口另含 total/start/end
3. 存储与响应契约
${SESSIONS_DIR}/_history/<id>/manifest.json是唯一持久提交点,格式为{ version: 2, revision, complete, resetRevision, session, entries }。session只有可序列化元数据,排除运行态句柄;entries只保留稳定消息 ID、角色、摘要、块引用、块内索引和字节数。- 完整消息保存在
chunks/<sha256>.json不可变块中,默认每块最多 2 MiB 或 64 条;单条超大消息允许独占大块,正文、工具、数组和深度不使用预览限额。保存复用内容未变的块,只写修改的消息,不序列化或复制整份历史。 - 旧
_history/<id>.json首次读取时使用lib/json-persistence.js按 64 KiB 扫描流式迁移,每次构造一个字段或消息;块全部落盘后才提交新清单。旧文件保留,迁移失败不破坏原文件。 <id>.json是有界快照缓存。historyArchiveRevision位于顶层首字段,用来判断缓存是否过期;不得从嵌套同名键推断修订。CC_WEB_SESSION_PERSIST_MAX_MESSAGES、CC_WEB_SESSION_MESSAGE_CONTENT_MAX_CHARS等预览配置不能限制完整档案;真实完成和恢复路径也不得先裁剪消息再提交。- 可信来源保存在存储实例的
WeakMap,不能凭 JSON 的修订字段伪造。基线只引用清单,不保留整份正文字符串。当前修订精确保存窗口内删除和插入,未加载前后缀原样保留;跨未知前缀的重复 ID 保留已提交原文。旧修订按单消息三方合并,保持活跃消息及工具对象引用。主动清空须明确markComplete(session, { reset: true })。 - 归档提交顺序为修改块写入、文件
fsync、原子rename,完成协调所需的块读取,再原子提交清单、推进基线、更新有界快照。清单失败不得推进基线或更新快照;已提交而快照失败仍返回成功。每32次提交回收无引用块,跨存储实例的活跃旧基线通过弱引用保护,清理失败不改变提交结果。 - 可信同修订保存仅读取最多256字节的修订头并复用不可变清单;修订已变化才读取当前清单。任务状态版本保护使用同一次提交的清单,不为单个字段重复加载历史或清单。普通保存保留更高任务版本,任务看板通过
taskBoardMetadata=true显式提交其负责的更新或删除;受保护状态仅在提交成功后同步到活跃对象。克隆只复制JSON容器,比较使用规范JSON值的结构比较,不为正文生成序列化副本。 - 列表枚举快照与档案 ID 的并集,排除
_turns保留目录及同名旧文件。首次快照失败后,孤立档案仍须可读、可列出;缓存继续失败时使用内存有界预览。 - Codex App 运行态写入
_history/_turns/<id>.json,包含完整正文、工具及映射,保留原version:1格式。读取直接构造对象,写入按小段转义正文并聚合写出,单次读写最多64KiB字节;不生成整份JSON原文或写出缓冲。运行态读取结果仍为完整对象,不能声称总内存与正文大小无关。有界运行态文件只作预览。重启枚举完整运行态,恢复消息提交成功后才清理,异常档案保留证据。 - 用户提问必须保存成功后才启动 runtime 或接受 steer;完成、重启恢复及跨会话返回必须检查保存结果,失败保留可重试内容。CLI 恢复标识须区分回合,同一已提交回合不可因清理失败再次追加。
- 确认原始边界后的旧前缀必须先进入自身档案。
complete=false保留旧缺口状态,不能因创建档案就宣称旧历史完整。 - 首屏和重连只读取最近12条;向前分页每次读取24条,目标消息预取受最大页数限制。表单和撤回使用单消息窗口,历史查重和附件枚举按块执行;任务看板只读取和保存元数据。
- 响应的
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:实际生产保存和读取入口、超限内容、存储重开、两类写失败、孤立档案、并发及活跃引用、删除/清空/撤回、确认前缀先归档。- 同一脚本还须覆盖默认尾部、中间窗口及元数据保存保留未加载历史,不同加载范围并发合并、清空防复活、失败后重试和未加载前缀 ID 去重。
- 同一脚本还须覆盖同修订保存只读修订头、更高任务版本整体保护、任务看板显式删除以及清单失败后任务状态和可信基线不提前推进。
scripts/json-persistence-unit.js:编码与读取对照原生JSON及项目bigint/Map/Set转换,覆盖toJSON单次求值、对象键、UTF-8和代理对跨段、短读短写、非法JSON及编码中途异常的原子失败保护。scripts/runtime-state-memory-unit.js:独立子进程在256MiB堆参数下读写80MiB旧v1完整运行态,正文与工具映射摘要逐项一致;检查单次64KiB字节读写及write/fsync/rename失败保留旧文件、无临时文件残留。合成峰值不能代表生产或任意并发量。scripts/session-history-memory-unit.js:独立子进程在256MiB堆上限下流式迁移70.5MiB旧归档,执行尾部/元数据保存和完整分页;逐条摘要校验所有历史,监测RSS、旧文件读取块大小和块复用。合成夹具的结果不能当作生产进程测量。- 真实完成与恢复路径使用超限正文和工具,断言归档原样读回;不能仅绕过生产入库函数直接构造消息数组。
- 真实 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 tail = store.loadTail(session.id, 180, { maxBytes: 2 * 1024 * 1024 });
const preview = buildSessionJsonForPersist({ ...tail, 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同时检查完整读取与有界实时预览。