上一篇分析了 App Server 的整体架构不同 Transport 被归一化成连接事件Message Processor 负责协议分发Request Processor 调用 CoreOutbound Router 再将 Response、Request 和 Notification 发送给客户端。本篇继续向协议内部深入。App Server 用 Thread、Turn、Item 三层对象描述一段 Agent 会话。它们看起来像普通的数据结构但真正理解这套协议需要同时回答四个问题每一层对象保存什么状态Request Response 与流式 Notification 如何配合客户端如何从增量事件还原最终界面断线恢复时持久化历史又如何变回相同的数据模型本篇将围绕这四个问题展开。本篇目标阅读完成后你应该能够区分 Thread、Turn、ThreadItem 的职责和生命周期。正确解释 ThreadStatus 与 TurnStatus。理解itemsView为什么是协议正确性的一部分。描述thread/start与turn/start的 Response/Notification 顺序。使用 Item ID 处理 Started、Delta 和 Completed 事件。避免用空的 Turn 快照覆盖客户端已收集的 Item。理解 Rollout History 如何重建 Thread、Turn 和 Item。1. 协议类型放在哪里核心类型位于codex-app-server-protocol文件职责thread_data.rsThread、Turn 聚合快照thread.rsThread API、状态和通知turn.rsTurn API、输入和通知item.rsThreadItem 联合类型和 Item 通知common.rsRequest 与 Notification Method 注册event_mapping.rsCore Event 到协议通知的映射thread_history.rsRollout History 重建这些文件说明协议存在四个不同层面API Method - Request / Response Type - Thread / Turn / Item Snapshot - Streaming Notification - Persisted History Reconstruction不要只阅读ThreadStruct。完整语义分布在快照、通知和重建逻辑中。2. 三层聚合关系最简化的数据结构是Thread id status metadata turns[] | - Turn id status items[] | - ThreadItem id type-specific fields其关系是一个 Thread 可以包含多个 Turn。一个 Turn 可以包含多个 Item。每个 Item 只属于一个 Turn。Turn 通过 Thread ID 归属 Thread。Item Notification 同时携带 Thread ID、Turn ID 和 Item ID。但这不意味着每个 Response 都携带完整嵌套树。为了控制传输和内存开销很多快照只包含元数据。3. Thread可持续的会话资源Thread 代表一段可以继续、恢复、分叉和归档的会话。主要字段可以按职责分组。3.1 身份关系字段含义idThread IDCodex 生成值为 UUIDv7sessionId同一 Session Tree 共享的 Session IDforkedFromIdFork 来源 ThreadparentThreadIdSubagent 的直接父 ThreadforkedFromId与parentThreadId不是同一个概念Fork 表示复制历史形成分支。Parent 表示多智能体 Spawn 关系。3.2 展示信息字段含义preview通常取第一条用户消息name用户可设置的 Thread 名称agentNicknameSubagent 随机昵称agentRoleSubagent 角色3.3 运行环境字段含义cwdThread 工作目录modelProvider模型提供方sourceCLI、VS Code、Exec、App Server 等来源threadSourceUser、Subagent、Feature、Memory ConsolidationgitInfo创建时捕获的 Git 信息cliVersion创建 Thread 的 CLI 版本3.4 持久化信息字段含义ephemeral是否仅存在于内存pathRollout 文件路径Ephemeral Thread 通常为nullhistoryModeLegacy 或 PaginatedcreatedAt创建时间秒updatedAt更新时间秒recencyAt用于排序的最近活跃时间秒3.5 运行状态和历史字段含义status当前 Thread Runtime 状态turns当前 Response 选择携带的 Turn 快照这里最容易误解的是turns。4.Thread.turns通常不是完整历史源码注释明确规定turns只在以下场景填充thread/resumethread/rollbackthread/forkthread/read且includeTurnstrue其他返回 Thread 的 Response 与 Notificationturns通常为空。特别是thread/start Response - turns 通常为空 thread/started Notification - 强制清空 turns thread/list - 元数据列表不承载完整历史thread_started_notification会在发送前主动执行thread.turns.clear()原因很直接创建或广播 Thread 时没有必要复制完整历史。多连接广播大对象会放大内存和带宽。历史应该通过显式 Read、Resume 或分页 API 获取。客户端不能看到turns[]就推断“这个 Thread 没有历史”。5. ThreadStatus 描述的是 Runtime不是最后一次任务ThreadStatus 定义为notLoaded idle systemError active { activeFlags }5.1 NotLoadedThread 存在于持久化存储但当前没有加载进 ThreadManager。5.2 IdleThread 已加载当前没有运行任务也没有等待中的交互。5.3 ActiveThread 正在运行或正在等待某类交互。activeFlags可能包含waitingOnApprovalwaitingOnUserInputActive 的 Flag 也可能为空。这表示 Thread 确实正在执行但暂时没有等待审批或用户输入。5.4 SystemErrorThread 已加载但 Runtime 记录了系统级错误且当前不再运行。5.5 状态优先级实现位于thread_status.rs简化优先级是未加载 - NotLoaded 正在运行或等待交互 - Active 存在系统错误 - SystemError 其他已加载状态 - IdleThreadStatus 不等于最后一个 Turn 的状态。例如上一个 Turn Failed但 Thread 仍可继续当前可能是 Idle。当前 Turn 正在运行Thread 是 Active。Thread 未加载时即使最后 Turn CompletedThread 仍是 NotLoaded。6. Turn一次有边界的用户任务Turn 表示 Thread 上的一次执行。核心字段字段含义idTurn IDCodex 生成值为 UUIDv7items当前 Payload 加载的 ItemitemsViewitems的完整度statusTurn 生命周期状态errorFailed 时的错误startedAt开始时间秒completedAt完成时间秒durationMs持续时间毫秒Turn ID 是客户端关联 Turn Notification 的关键不应使用数组位置代替。7. TurnStatus 是一次任务的终态TurnStatus 包含inProgress completed interrupted failedInProgressTurn 已提交或已开始但尚未进入终态。CompletedTurn 正常完成。它表示 Agent 循环结束不代表每个工具都一定成功。例如某个命令可能失败但 Agent 读取失败结果后仍生成最终回复Turn 仍可 Completed。InterruptedTurn 被用户中断或通过turn/interrupt取消。FailedTurn 因不可恢复错误结束。此时error应包含message可选codexErrorInfo可选additionalDetailsTurn 的 Error 只在 Failed 状态下有意义。8.itemsView不能被忽略同一个 Turn 可以只携带不同详细程度的 Item。TurnItemsView包含NotLoadeditems []表示 Item 没有加载而不是 Turn 没有 Item。Summary只保留适合列表展示的摘要第一条 UserMessage。最后一条 AgentMessage。如果二者之一不存在则只返回存在的项。Full包含持久化历史中当前可用的完整 Item 列表。客户端必须同时检查turn.items turn.itemsView不能只检查items.length。9. 分页历史为什么需要itemsViewthread/turns/list默认按降序分页 Turn并允许选择itemsView notLoaded | summary | full这样不同界面可以选择不同成本Thread 列表不加载 Item。Resume Picker加载摘要。会话详情加载完整 Item。thread/items/list则直接按 Item 分页还可以指定turnId。两种 API 的区别API分页对象适用场景thread/turns/listTurn会话时间线、Turn 摘要thread/items/listItem大会话的详细内容Cursor 是不透明字符串客户端不应解析或自己计算。10. Turn Start 的 Response 只是“已接受”客户端发送{method:turn/start,id:10,params:{threadId:THREAD_ID,input:[{type:text,text:解释这个模块,textElements:[]}]}}TurnRequestProcessor将输入转换为 CoreOp::UserInput提交给CodexThread然后立即构造status inProgress items [] itemsView notLoaded startedAt null completedAt nullResponse 表示请求参数有效。用户输入已经提交给 Thread。Server 已分配 Turn ID。它不表示模型已经开始采样。11.turn/started才表示 Runtime 真正开始Core 发出EventMsg::TurnStarted后App Server 才发送turn/startedNotification 包含threadIdTurn Snapshot此时status inProgressstartedAt通常已经可用items[]itemsViewnotLoaded所以一个 Turn 存在两个启动时刻turn/start Response - 已提交并获得 Turn ID turn/started Notification - Core Runtime 已开始执行客户端可以在 Response 后立即创建占位 Turn在 Notification 到达后补充 Runtime 开始状态。但客户端不应把网络到达顺序写成强前置条件。Response 处理与 Core Event Listener 属于不同异步路径快速 Turn 可能让两条消息非常接近。无论哪条先到都应根据同一个 Turn ID 执行 Upsert。12. Turn Start 可以携带哪些输入TurnStartParams.input是UserInput数组。支持Text。Image URL。Local Image。Skill。Mention。Text 还可以携带textElements用字节范围标记 UI 中的特殊元素。Turn Start 还可以覆盖后续 Sticky SettingsCWD。Workspace Roots。Approval Policy。Approval Reviewer。Sandbox 或 Permission Profile。Model。Service Tier。Reasoning Effort。Reasoning Summary。Personality。Collaboration Mode。Output Schema。这些 Override 不只是当前请求的临时参数部分会成为 Thread 后续 Turn 的设置。13.clientUserMessageId解决什么问题客户端可以在turn/start或turn/steer中传入clientUserMessageId对应 UserMessage Item 会回显clientId它可以用于客户端乐观渲染后去重。将本地消息与 Server 回显关联。避免仅根据文本内容判断是否为同一消息。文本相同不代表消息相同稳定 Client ID 比文本去重更可靠。14. ThreadItem 是可判别联合类型ThreadItem使用type字段区分变体。主要类型可以分成五组。14.1 对话内容userMessagehookPromptagentMessageplanreasoning14.2 本地工具commandExecutionfileChangeimageViewsleep14.3 外部工具mcpToolCalldynamicToolCallwebSearchimageGeneration14.4 多智能体collabAgentToolCallsubAgentActivity14.5 生命周期标记enteredReviewModeexitedReviewModecontextCompaction所有变体都拥有稳定 Item ID但不同类型拥有不同字段和状态。15. Item 没有一个统一 StatusThreadItem Enum 本身没有公共status字段。只有需要生命周期的类型才定义自己的状态。CommandExecutioninProgress completed failed declined还包含Command。CWD。Process ID。Aggregated Output。Exit Code。Duration。FileChangeinProgress completed failed declined并携带 Add、Delete、Update Diff。McpToolCallinProgress completed failed并携带 Arguments、Result、Error 和 Duration。DynamicToolCallinProgress completed failedCollabAgentToolCallinProgress completed failedAgentMessage、Reasoning 或 UserMessage 不需要额外状态它们通过 Item 生命周期事件表达完成。16. Item 生命周期Started、Delta、Completed一个典型流式 Agent Messageitem/started item.type agentMessage item.id item-1 item/agentMessage/delta itemId item-1 delta 第一段 item/agentMessage/delta itemId item-1 delta 第二段 item/completed item.id item-1 item.text 第一段第二段工具调用也采用相同思路item/started - statusinProgress 类型专属进度通知 - outputDelta / patchUpdated / progress item/completed - final status final payload并非每个 Item 都保证同时存在 Started 和 Completed。某些瞬时事件可以直接以 Completed Item 出现。客户端应当按 Item ID Upsert而不是假设事件严格成对。17. Completed Item 是最终权威快照Delta 用于低延迟显示Completed Item 用于最终一致性。正确策略是Started 创建临时 Item。Delta 更新临时展示 Buffer。Completed 使用完整 Item 替换临时版本。不要永久依赖 Delta 拼接结果。Plan的协议注释尤其明确PlanDelta 拼接结果不保证等于 Completed Plan 文本原因可能包括Server 端归一化。重试或替换。内容过滤。最终结构转换。AgentMessage 也应以 Completed Item 作为最终快照。18. Item Notification 的时间单位Item 生命周期 Notification 使用startedAtMscompletedAtMs单位是毫秒。而 Thread 和 Turn 的createdAtupdatedAtstartedAtcompletedAt单位是秒。这是客户端实现中很容易出现的错误Thread/Turn timestamp - seconds Item lifecycle - milliseconds Turn duration - milliseconds不要直接把所有数字交给同一个 Date Constructor。19. Core Event 如何变成 Item Notification映射入口是event_mapping.rs它处理可以一对一转换的 Core Event例如EventMsg::ItemStarted - item/started EventMsg::ItemCompleted - item/completed EventMsg::AgentMessageContentDelta - item/agentMessage/delta EventMsg::ExecCommandOutputDelta - item/commandExecution/outputDelta一些需要状态或副作用的事件由bespoke_event_handling.rs处理例如Turn Started/Completed。Pending Approval 清理。Interrupt Response。Thread Status 更新。命令兼容事件去重。这种拆分的原则是无状态一对一映射放在 Protocol Crate。需要 Runtime State 的映射留在 App Server。20.turn/completed不携带完整 ItemTurn 完成时App Server 构造id 当前 Turn ID items [] itemsView notLoaded status completed | interrupted | failed error Failed 时的错误 startedAt 已知开始时间 completedAt 完成时间 durationMs 持续时间这是本篇最重要的协议细节之一。turn/completed的职责是宣布 Turn 终态不是重新发送完整 Turn History。如果客户端执行state.turns[turnId] notification.turn就会把之前通过 Item Notification 收集的所有 Item 覆盖为空。正确做法是只合并终态字段status error startedAt completedAt durationMs除非itemsView明确表明 Payload 携带了所需历史否则不要覆盖已构建的 Item 列表。21. 一次完整 Turn 的典型事件序列客户端可能观察到turn/start Response turn/started item/completed UserMessage item/started Reasoning item/reasoning/summaryTextDelta ... item/completed Reasoning item/started CommandExecution item/commandExecution/outputDelta ... item/completed CommandExecution item/started AgentMessage item/agentMessage/delta ... item/completed AgentMessage turn/completed实际序列会因工具、模型和 Feature 不同而变化可能没有 Reasoning。可能有多个命令。可能发生 MCP 调用。可能发起审批 Request。可能有子智能体。可能 Turn Failed 或 Interrupted。客户端应围绕 ID 和类型编写 Reducer不应硬编码固定 Item 顺序。22. ThreadStatus 与 TurnStatus 如何配合典型状态变化Thread Idle | | turn/start v Thread Active, Turn InProgress | | approval request v Thread Active(waitingOnApproval), Turn InProgress | | approval response v Thread Active, Turn InProgress | | turn/completed v Thread Idle, Turn Completed如果 Turn FailedThread Active - Thread SystemError 或 Idle Turn InProgress - Turn Failed具体 Thread 状态取决于 Runtime Error 是否仍被记录不能只由 Turn Failed 机械推导。23.turn/interrupt与turn/steer23.1 Interrupt请求必须同时提供Thread ID。Turn ID。这防止客户端误中断同一 Thread 上已经切换的新 Turn。成功 Response 是空对象。真正终态通过turn/completed status interrupted通知。23.2 SteerSteer 向正在运行的 Turn 追加用户输入。它要求expectedTurnId如果当前活跃 Turn 已变化请求会失败。成功 Response 返回真正接受输入的 Turn ID。Review 和手动 Compaction Turn 不接受 Steer。24. Thread Start 的 Response 与 Notificationthread/start成功时Server 先发送 ResponseThread 快照。实际 Model。Model Provider。Service Tier。CWD。Workspace Roots。Instruction Sources。Approval Policy。Sandbox。Permission Profile。Reasoning Effort。随后发送thread/startedNotification 中只保留 Thread 快照并清空turns。这种设计让请求发起方获得完整启动配置其他订阅方只获得生命周期通知。25. Resume 与 Fork 为什么更复杂ResumeResume 可能根据 Thread ID 加载。根据 Path 加载。使用实验性内存 History。重新加入当前正在运行的 Thread。Response 可以携带历史 Turn。ForkFork 会复制源 Thread 历史。分配新 Thread ID。设置forkedFromId。可选择只复制到某个 Turn。可创建 Ephemeral Fork。Fork 的thread/startedNotification 同样不会广播完整历史。运行中 ThreadResume 一个正在运行的 Thread 时Server 需要同时处理持久化历史。当前内存中的 Active Turn。已经发生但尚未完全落盘的 Item。Pending Approval。新连接订阅。这也是ThreadHistoryBuilder同时服务持久化重放和当前 Turn 追踪的原因。26. ThreadHistoryBuilder从事件日志重建快照thread_history.rs 实现一个 Reducer。输入包括RolloutItem::EventMsgRolloutItem::CompactedRolloutItem::ResponseItemTurn Context 等元数据Reducer 按顺序处理User Message。Agent Message。Reasoning。Web Search。Command Begin/End。Patch Begin/End。MCP Begin/End。Turn Started/Complete/Aborted。Rollback。Compaction。最终生成VecTurn26.1 为什么不能直接反序列化成 TurnRollout 保存的是事实流不是每一步完整快照。例如命令执行可能分散为ExecCommandBegin Output Delta ExecCommandEndReducer 需要将它们合并成一个最终 CommandExecution Item。26.2 Item Upsert同一 Item ID 的后续状态会替换前一状态而不是新增重复 Item。26.3 RollbackRollback 会删除被回退 Turn并清除对应的增量 Change Set。26.4 Incremental Change SetBuilder 不仅能一次性生成全部历史还能返回Changed Items。Changed Turns。Removed Turn IDs。这允许运行中的 Thread 只更新受影响快照。27. 客户端 Reducer 应该如何设计建议将客户端状态拆成threadsById turnsById itemOrderByTurnId itemsById itemDeltaBuffersThread Startedupsert thread metadata 不要用空 turns 删除已有历史Turn Start Responseupsert placeholder turn status inProgressTurn Startedmerge runtime start fields mark thread activeItem Startedif item id is new: append id to turn order upsert itemItem Deltalocate by threadId turnId itemId append or update type-specific bufferItem Completedupsert final item snapshot clear matching delta bufferTurn Completedmerge status/error/time fields 保留本地已收集 itemsThread Status Changedreplace thread runtime status 不要修改 turn status这种 Normalized State 比嵌套数组更适合增量更新也避免每个 Delta 复制整棵 Thread Tree。28. 一个简化的 Reducer 伪代码on ItemStarted(event): turn turns[event.turnId] if event.item.id not in turn.itemIds: turn.itemIds.push(event.item.id) items[event.item.id] event.item on ItemCompleted(event): ensureItemOrder(event.turnId, event.item.id) items[event.item.id] event.item deltaBuffers.remove(event.item.id) on TurnCompleted(event): turn turns[event.turn.id] turn.status event.turn.status turn.error event.turn.error turn.completedAt event.turn.completedAt伪代码有意不执行turn.items event.turn.items因为实时turn/completed的 Items 没有加载。29. 如何处理重复与乱序风险协议在单连接、单 Thread Listener 内尽量保持事件顺序但客户端仍应具备基本幂等性。建议Thread、Turn、Item 全部按 ID Upsert。Completed 可以在没有 Started 时创建 Item。重复 Started 不重复插入 Item Order。重复 Completed 以最新完整快照覆盖。未知 Delta 先进入临时 Buffer等待 Started 或 Completed。Turn Completed 不删除仍在本地的 Item。Resume 后以 Server History 校正本地缓存。对于多连接或断线重连场景ID 比事件计数器更可靠。30. 实时事件与持久化历史并不完全相同实时协议强调低延迟Started。Delta。Progress。Completed。持久化历史强调可恢复事实完整 User/Agent Message。工具开始与结束。Turn Context。Compaction。Rollback。并非每个 UI 进度事件都必须永久保存。因此恢复后的界面应保证语义一致但不一定逐字重放所有瞬时进度。例如命令最终输出可以恢复。曾经显示过的 Loading 文案不一定恢复。Delta 可以恢复为最终完整消息。31. Experimental 字段如何进入协议Thread、Turn 和 Item 中存在多个 Experimental 字段。协议类型使用ExperimentalApi元数据标记某个 Method 是否实验性。某个字段是否实验性。嵌套类型是否包含实验字段。客户端未在 Initialize 中启用 Experimental API 时实验性 Method 会被拒绝。实验性 Notification 会被过滤。某些出站 Payload 会移除实验字段。这允许稳定客户端继续使用同一 Protocol Version而不被未完成字段强制绑定。32. Schema 生成为什么很重要App Server Protocol 同时派生Serde Serialize/Deserialize。JSON Schema。TypeScript Type。可以通过codex app-server generate-ts--outDIR codex app-server generate-json-schema--outDIR生成与当前 Codex 二进制版本匹配的协议定义。外部客户端应优先使用生成类型而不是手写Method Name。Camel Case 字段。Nullable 与 Optional。Experimental 字段。ThreadItem Union。尤其是 ThreadItem 变体较多手写类型很容易漏掉新增 Item。33. 常见误区误区一ThreadStatus 就是最后一个 TurnStatusThreadStatus 描述 Runtime 是否加载、运行或等待交互TurnStatus 描述一次任务结果。误区二items[]表示没有内容必须同时检查itemsView。NotLoaded 表示内容没有加载。误区三turn/startResponse 表示模型已开始Response 表示提交成功真正开始由turn/started通知。误区四turn/completed包含完整 Turn实时完成通知中的 Items 明确为 NotLoaded。误区五所有 Item 都有统一 Status只有命令、文件、MCP 等有执行生命周期的变体拥有类型专属状态。误区六拼接 Delta 就是最终内容Completed Item 才是最终权威快照Plan 尤其不保证 Delta 拼接等于最终文本。误区七每个 Item 都一定先 Started 再 Completed部分瞬时 Item 可以直接 Completed客户端必须按 ID Upsert。34. 动手练习练习一绘制三层状态机分别画出ThreadStatus。TurnStatus。CommandExecutionStatus。标记哪些状态可以并存。练习二跟踪一次 Agent Message从 CoreAgentMessageContentDelta跟踪到item/agentMessage/delta再找到 Completed AgentMessage 如何构造。记录 Thread ID、Turn ID 和 Item ID 在每层的来源。练习三验证空 Items 语义阅读以下三处TurnRequestProcessor创建 TurnStartResponse。TurnStartedEvent Handling。emit_turn_completed_with_status。确认它们为什么都使用itemsView notLoaded练习四实现内存 Reducer用任意语言实现以下事件Thread Started。Turn Started。Item Started。Agent Message Delta。Item Completed。Turn Completed。要求 Turn Completed 后仍能读取完整 Agent Message。练习五比较历史视图对同一个 Thread 分别请求itemsViewnotLoaded itemsViewsummary itemsViewfull比较 Payload 大小和 Item 内容。35. 本篇小结Thread、Turn、Item 不只是三层嵌套数据而是一套“快照加事件”的协议模型。核心规则可以概括为Thread 表示可持续、可恢复和可分叉的会话。Turn 表示一次有终态的用户任务。ThreadItem 表示对话、工具和生命周期内容。ThreadStatus 与 TurnStatus 描述不同层次不能互相替代。itemsView决定空 Items 是“没有内容”还是“未加载”。turn/startResponse 表示提交成功turn/started表示真正运行。Started 和 Delta 用于实时展示Completed Item 是最终权威快照。turn/completed只携带终态元数据不携带完整 Item。客户端应按 ID Upsert并使用 Normalized State 避免重复复制。ThreadHistoryBuilder 将持久化事实流重建为相同的 Turn/Item 模型。下一篇将进入 App Server 到 Core 的调用边界跟踪一次 JSON-RPC Request 如何经过 MessageProcessor、ThreadRequestProcessor、ThreadManager最终创建并返回一个可运行的 Core Thread。