重构directproject的thread持久化, 保存response items用来恢复thread 修改了DirectProject的对话jsonl格式, 不兼容不迁移. (测试需要手动清理旧对话jsonl文件) AGC显式写入user msg, 对codex 返回的user msg忽略 清理原来的上下文滑动窗口, 因为codex内部会自动compact 工具调用后重启,继续对话:  close #249 close #277 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: #282 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
4.3 KiB
DirectProject Codex 原始历史与异常恢复
更新时间:2026-09-07
目标
DirectProject 只使用 .agent/conversations/project.jsonl 作为对话历史。历史保存 Codex Responses API 的完整 item,使聊天展示与新线程恢复使用同一份事实来源;两者只是不同读取动作。
本方案只适用于 DirectProject,不改变 DirectHome、Agent session 历史或 runtime/direct-codex/turns 审计账本。
文件格式
每行采用 Codex CLI rollout 的最小事件外壳,不保存 AGC 自有的顺序号或运行环境字段:
{"type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"你好"}]}}
payload 必须是未经改写的 Responses item。Direct 回合不由浏览器预写用户 message;Codex 返回的 rawResponseItem/completed.params.item 原样追加。显式的本地 user/assistant 补写只能通过受权限保护的 append_direct_project_conversation_message 命令完成。native 工具、MCP 工具、reasoning、调用参数和调用结果都保留完整内容,不截断、不摘要、不保存 delta/started 事件。
DirectProject 不迁移旧 {role,content} 行;实现按新格式工作。
正常回合
- 启动
ephemeral: true线程,并启用experimentalRawEvents: true。 - 新线程先把历史 item 数组一次注入;注入成功后执行新的
turn/start。本轮用户 item 只接受 Codex 回传的rawResponseItem/completed,不由 AGC 预写。 - 收到
rawResponseItem/completed后立即追加其params.item并 flush。 - 正常
turn/completed: completed不生成额外记录。
异常回合收尾
AGC 判定本轮不会再产生新事件时收尾:用户中断、turn failed、无响应/idle timeout、硬超时、transport closed、stdout EOF 或 app-server 卡死终止均属于异常终态;正常 completed 不收尾。
item/agentMessage/delta 正常带有 itemId;若协议异常缺失,AGC 记录 warning 并按当前 turn 生成稳定回退 id。AGC 在内存中按该 id 累计 assistant 文本,不实时写 delta。异常终态时,对仍有累计文本的 item 合成普通 Responses assistant message item:
{"type":"response_item","payload":{"type":"message","role":"assistant","id":"msg_1","content":[{"type":"output_text","text":"已累计文本"}]}}
合成 item 在返回错误、销毁连接或启动恢复线程前追加并 flush。没有文本 delta 的半截工具/MCP 调用不合成,等待完整 rawResponseItem/completed。
rawResponseItem/completed 缺少 item(包括 null)时视为反序列化错误;该回合按异常终态收尾,历史中不会写入非法空 item。
Codex 启动时注入的 host_skills.instructions、permissions.instructions 和 environments.environment_context item 不属于项目对话历史;落盘时过滤,读取和线程注入时也过滤。过滤同时识别 role=user 的完整上下文标签包裹文本,即使该 item 没有 internal_chat_message_metadata_passthrough 元数据,也不能把它当成用户回合。
恢复
创建新的 ephemeral thread 后,读取 project.jsonl 中所有 response_item.payload,按文件行顺序一次调用 thread/inject_items,再执行新的 turn/start。Codex 负责上下文窗口管理;注入失败直接失败,AGC 不截断、摘要或改写历史。新 thread 已进入连接池但历史读取或注入失败时,必须先从池中淘汰并取消订阅该 thread,重试只能创建新 thread 并重新注入。
clientUserMessageId 仅作为 Codex 用户消息的稳定标识随 turn/start 发送,不等价于 turn 级 exactly-once 幂等。断线后的重试仍须由项目侧持久化 turn ledger 或服务端去重合同决定,不能仅凭该字段再次执行。
聊天界面只从 message item 提取 user/assistant 内容;工具 item 不再拼成 tool: ... 假文本。
DirectProject 的浏览器层只负责显示和乐观状态,不再调用通用对话写入器;Rust 是该历史文件的唯一写入方。历史读写与回合累计分别位于 agent/direct_project_history.rs 和 agent/direct_project_turn_history.rs。
写入与损坏边界
写入使用 write_all + flush。读取时允许丢弃文件末尾一条不完整 JSON 行;中间坏行直接失败。不会对旧格式做迁移或兼容。