Codex 当前真正稳定存在的两个协议身份是 ThreadId 和 SessionId。源码里没有一个仍在生产路径上独立定义的 ConversationId 类型;conversation_id 只残留在恢复历史、旧事件字段和兼容命名中。把三者写成三个并列 ID,会直接误导后续对恢复、派生 Agent 和 Rollout 的理解。
先给结论:身份、对象和历史名字不是一回事
| 名称 | 当前载体 | 生命周期 | 主要用途 |
|---|---|---|---|
| Thread 身份 | ThreadId | 跨加载、跨恢复持久存在 | 定位一条对话线程和它的 Rollout |
| Session 树身份 | SessionId | 根 Agent 与其后代共享 | 把一个多 Agent 工作树归到同一会话 |
| 活体 Runtime | Session 对象 | Thread 本次被加载到进程期间 | 持有通道、状态、服务和活动 Turn |
| 旧 Conversation 命名 | conversation_id 字段 | 兼容历史格式 | 在恢复数据中承载 ThreadId |
ThreadId 和 SessionId 都包装 UUID,并以字符串序列化。默认构造使用 UUIDv7,不只是为了随机唯一性,也让按值排序大致保留创建时间。两者可以显式相互转换,但“能转换”不代表“语义相同”。
新建、恢复和派生走不同的身份选择
Session::new 不会无条件生成两个新 ID。Thread 身份取决于初始历史:
- New、Cleared、Forked 创建新的
ThreadId; - Resumed 直接采用恢复记录中的
conversation_id; - Rollout 的
SessionMeta若带有持久化 SessionId,会参与会话树身份恢复; - 非根 Agent 新建时从
AgentControl继承根会话的 SessionId; - 根 Session 没有已保存 SessionId 时,才用自己的 ThreadId 初始化 SessionId。
这解释了为什么根线程刚创建时两个值经常相同,却不能据此把它们永远等同。子 Agent 拥有自己的 ThreadId,但继承根 Agent 的 SessionId。
def choose_identities(initial_history, session_meta, agent_control, source):
if initial_history.kind == "resumed":
thread_id = ThreadId(initial_history.conversation_id)
else:
thread_id = ThreadId.uuid_v7()
persisted = session_meta.session_id if session_meta else None
if source.is_descendant_agent:
session_id = agent_control.session_id()
elif persisted is not None:
session_id = persisted
else:
session_id = SessionId.from_thread_id(thread_id)
return thread_id, session_id
源码还专门过滤一类旧 Rollout:历史子 Agent 可能把自己的 ThreadId 错当成 SessionId 保存。若恢复时机械相信这个值,会把原本属于同一根会话的 Agent 拆成多个会话树。兼容逻辑因此不是简单字段改名,而是带来源判断的数据修复。
为什么恢复的是 Thread,而不是原 Session 对象
ThreadManager 可以从 Rollout 重新构造 Session。恢复前后的 Rust 对象、Channel、Tokio 任务和内存锁都不是同一份,但 ThreadId 保持不变。由此可得到一个严格边界:
async def resume_thread(thread_id):
live = live_threads.get(thread_id)
if live is not None:
return live
history = await thread_store.read_rollout(thread_id)
session = await Session.spawn(initial_history=Resumed(history))
assert session.thread_id == thread_id
return register_live_session(session)
所以“一个 Thread 对应一个 Session”只在“当前已加载的 live runtime”这个时间切片内成立。持久化 Thread 可以先后对应多个 Session 实例;一个 Session 实例则固定服务于一个 Thread。
设计代价与失败边界
双身份的收益是 Thread 可以独立恢复、归档和分叉,同时多 Agent 运行仍能按根会话聚合。代价是任何存储和遥测代码都必须明确自己需要“线程”还是“会话树”:用 SessionId 查单条 Rollout 会过宽,用 ThreadId 聚合整棵 Agent 树又会漏掉后代。
恢复时还有三个必须拒绝或修复的情况:非法 UUID 字符串、历史元数据与恢复目标冲突、旧子 Agent SessionId 污染。源码的做法是把字符串解析集中在强类型包装器,把历史兼容留在 Session 构造处,而不是让业务代码到处猜测。
源码与测试锚点
codex-rs/protocol/src/thread_id.rs:ThreadId、UUIDv7 与字符串序列化。codex-rs/protocol/src/session_id.rs:SessionId 及双向显式转换。codex-rs/core/src/session/session.rs:新建、恢复、根/子 Agent 的身份选择。codex-rs/core/src/thread_manager.rs:按 ThreadId 恢复和注册 live thread。
评论
登录后即可评论