Files
Genarrative/docs/technical/【技术方案】DirectProject Codex原始历史与异常恢复-2026-09-04.md
T
k88936 d3f5d0a355
Project CI / Repository checks (push) Successful in 2m7s
Project CI / Frontend tests (push) Successful in 2m46s
Project CI / Backend tests (push) Failing after 3m53s
Project CI / Native shell tests (push) Failing after 3m50s
feat/AGC codex的工具调用 持久化处理 (#282)
重构directproject的thread持久化,  保存response items用来恢复thread

修改了DirectProject的对话jsonl格式,  不兼容不迁移. (测试需要手动清理旧对话jsonl文件)

AGC显式写入user msg, 对codex 返回的user msg忽略
清理原来的上下文滑动窗口, 因为codex内部会自动compact

工具调用后重启,继续对话:
![shotmd-1788579616.jpg](/attachments/8350c924-9498-47cd-b3da-8be6b79696f9)

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>
2026-09-08 22:04:37 +08:00

4.3 KiB
Raw Blame History

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} 行;实现按新格式工作。

正常回合

  1. 启动 ephemeral: true 线程,并启用 experimentalRawEvents: true
  2. 新线程先把历史 item 数组一次注入;注入成功后执行新的 turn/start。本轮用户 item 只接受 Codex 回传的 rawResponseItem/completed,不由 AGC 预写。
  3. 收到 rawResponseItem/completed 后立即追加其 params.item 并 flush。
  4. 正常 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.instructionspermissions.instructionsenvironments.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.rsagent/direct_project_turn_history.rs

写入与损坏边界

写入使用 write_all + flush。读取时允许丢弃文件末尾一条不完整 JSON 行;中间坏行直接失败。不会对旧格式做迁移或兼容。