v0.3.3 — Local WS stateful protocol implementation + transport-layer refactor
Fix
- Local implementation of Codex 0.142's stateful
responses_websocketsprotocol. Each turn, Codex first sends agenerate:falseprewarm frame, and when the connection is reused it compresses the request into aprevious_response_id+ incremental (possibly empty) input. The old version forwarded these verbatim to the stateless SSE upstream, so prewarm frames were rejected with a 400 (Unsupported parameter: generate) and incremental frames were forwarded as if they were full input, making the model lose its history/context. NowWsSession(one per downstream connection) digests this protocol locally: prewarm frames are answered locally with a synthetic completed response (resp_codexcomp_prewarm_*); incremental frames are rebuilt to full input fromlast_input + last_output + deltabefore enteringfold(); unknown ids fail loudly and close the connection, forcing Codex to reconnect with full input. Failed/aborted requests invalidate the stored state, matching Codex's behavior of only recordingLastResponseon Completed. The upstream only ever sees stateless, full requests. - Real-hardware verification (codex 0.142.5, official subscription): against v0.3.2 this reproduced 2 400s; the fixed version ran 63 requests with all 8
518n−2folds completing, full context across multi-turn resume, and 0 failures.
Refactor
- Terminal events are now solely owned by
fold(): when round 1 is rejected by the upstream,fold()mints its ownresponse.failed, andRoundOpenErrorno longer escapes to the transport layer; the threeresponse.incompleteemission sites are converged into a single helper. Every downstream terminal event shape now has exactly one owner, and the error paths are testable directly through thefold()interface. - Extracted the
drive_foldfold driver: theUpstreamRoundslifecycle (construction, iterating the fold, teardown/close) is collected into one shared async generator, and the SSE and WebSocket handlers degenerate into thin adapters that only do frame encoding, removing two copies of byte-for-byte duplicated driver logic. Along the way theaccept-header "force-set then pop" round trip is dropped, and the passthrough path keeps the client's originalaccept. - Explicit assembly:
build_app(upstream_base)takes the parameter explicitly (falling back to theCODEXCOMP_UPSTREAM_BASEenv var, then the official backend), removing the module-level globalappand the "CLI writes the env var, import reads it" load-order coupling;DEFAULT_HOST/DEFAULT_PORT/DEFAULT_UPSTREAMare converged into a single source of truth incodexcomp/__init__.py. - Removed
--auto-port: a wired proxy must exclusively own the exact port that matchesopenai_base_url, so a port-drift branch contradicts the contract; when the port is busy it keeps failing loudly and exiting.
Tests
- Added
test_ws.py: covers prewarm, empty incremental, tool-loop, unknownprevious_response_id, and post-failure state invalidation. test_fold.pyadds error-path cases: round 1 rejected →response.failed, continuation open failed →incomplete/upstream_error(reasoning retained, truncated message not released), and EOF with no terminal →incomplete/upstream_eof.
Logging additionally emits cached_tokens.
中文说明
本版在代理侧本地实现了 Codex 0.142 的有状态 responses_websockets 协议(prewarm 本地应答、增量帧重建为全量、未知 id 响亮失败),使上游始终只见无状态全量请求,真机 63 请求 8 次折叠全通过。同时把所有下游终局事件收归 fold() 唯一所有、提取共享的 drive_fold 驱动器、显式化 build_app 装配并移除 --auto-port,另补 test_ws.py 与 test_fold.py 错误路径用例。