response.created 出现 ID 并不代表它可作为下一请求的可靠父节点。Codex 只在 Completed 时把 response_id 与所有 Done Item 一起提交给 WebsocketSession;任何断流、重连或消费失败都会让引用链失效。
具体问题与启用条件
本节说明 ID 何时提交、一次性 receiver 怎样交接,以及服务端找不到 previous response 时如何回到完整请求。
| 条件来源 | 决定字段或状态 | 对本机制的影响 |
|---|---|---|
| 响应终态 | ResponseEvent::Completed | 提交 LastResponse |
| 请求准备 | last_response_rx 可立即取到结果 | 允许评估 previous response |
| 服务端 | previous_response_not_found | 触发可重试并在下次发送完整请求 |
协议、类型与状态所有权
| 状态或协议 | 所有者 | 生命周期 | 关键不变量 |
|---|---|---|---|
| response_id + items_added | map_response_events | 一次完成响应 | 通过 oneshot 恰好提交一次 |
| last_response_rx | WebsocketSession | 到下一次 prepare | 被 take 后不可重复消费 |
| last_response_from_untraced_warmup | WebsocketSession | 到下一次请求 | 决定 trace 记录完整逻辑请求 |
机制调用链如下:
OutputItemDone 累积 items_added
→ Completed 到达
→ oneshot.send(LastResponse)
→ WebsocketSession 保存 receiver
→ 下一请求 try_recv + take
→ 严格匹配后取 response_id
→ 发送 previous_response_id
→ 新响应 Completed 覆盖链头
机制怎样工作
Core 的 Created 事件不保存可复用 ID;真正的提交点在流映射器收到 Completed 时。它先记录 trace/usage,再通过 oneshot 发送 {response_id, items_added},随后把 Completed 转交 Runtime。这样 ID 与组成服务端上下文的完整项保持原子边界。
准备下一请求时 receiver 被 take() 并 try_recv()。Empty、Closed、空 ID 都表示没有可靠父响应,当前请求发完整 Input。若 previous response 来自未进入 inference trace 的预热,线传输仍可用空 delta,但 trace 必须记录完整逻辑请求。服务端返回 previous_response_not_found 时,WS 适配器映射为 Retryable;本次 last_response receiver 不会产生 Completed,下一次尝试取不到父响应,自然退回完整请求。
Python 风格伪代码
这段伪代码保留生产实现中会改变结果的状态、分支和异步边界;认证 SDK、遥测字段和 Rust 所有权样板被折叠为明确的领域对象。
async def map_stream(events, response_slot: FutureSlot):
done_items: list[ResponseItem] = []
async for event in events:
if isinstance(event, OutputItemDone):
done_items.append(event.item)
if isinstance(event, Completed):
response_slot.set_once(LastResponse(event.response_id, done_items.copy()))
yield event
def take_previous(state: WsState) -> LastResponse | None:
slot, state.last_response_slot = state.last_response_slot, None
return slot.try_result() if slot is not None else None
async def send_with_previous(request: Request, state: WsState):
previous = take_previous(state)
incremental = compare_and_slice(request, state.last_request, previous)
try:
return await send(incremental or full(request))
except PreviousResponseNotFound:
state.last_response_slot = None
raise RetryableError("retry full request")
失败、取消与恢复
| 故障或边界 | 已发生的状态 | 对上层的结果 | 能否直接重试 | 恢复动作 |
|---|---|---|---|---|
| 流在 Completed 前断开 | 可能已累积 Done Item | 不提交 LastResponse | 是 | 下一尝试完整请求 |
| oneshot Closed/Empty | 没有可读父响应 | 禁用本次增量 | 不报致命错 | 发送完整请求 |
| response_id 为空 | Completed 数据不完整 | 禁用增量 | 不适用 | 完整请求 |
| previous_response_not_found | 服务端无父节点 | Retryable | 是 | 消耗旧 slot 后完整重发 |
设计取舍与验证
只在 Completed 提交会错过用 response.created 提前串联的性能机会,却避免引用一个失败或不完整响应。oneshot 精确表达“一次响应只被下一次准备消费一次”,但要求请求准备与流完成顺序正确;try_recv 的保守回退保证竞态不会产生错误增量。
| 可验证契约 | 证据方式 | 预期结果 |
|---|---|---|
| Completed 才产生 LastResponse | 映射器事件序列测试 | 半流 receiver 无值 |
| 预热 Response ID 支持空 delta | 首 Turn 请求捕获 | previous 有值且 input 为空 |
| 前缀失配废弃 previous | 修改历史后请求 | 完整 input 且无 previous |
Mini Codex 对照
Mini Codex 可用 asyncio.Future[LastResponse],但读取后必须清空槽位。不要在 Created 时写入 ID;如果服务端报告 previous 不存在,应把它归为一次可恢复的完整重发。
本节边界
ID 的提交和失效已经闭合。5.12 再向外看:何时整个 WebSocket 路径被 Session 级 HTTP 回退替代。
评论
登录后即可评论