Skip to content

compare function_call arguments semantically in codex continuation prefix check - #119

Merged
raine merged 3 commits into
raine:mainfrom
ItsAlbertZhang:fix/codex-continuation-function-call-arguments
Aug 23, 2026
Merged

compare function_call arguments semantically in codex continuation prefix check#119
raine merged 3 commits into
raine:mainfrom
ItsAlbertZhang:fix/codex-continuation-function-call-arguments

Conversation

@ItsAlbertZhang

Copy link
Copy Markdown
Contributor

Fixes #118: with CCP_CODEX_PREVIOUS_RESPONSE_ID=1, the turn after a tool call falls back to a full-context request (not_append_only) whenever the model did not emit the call's arguments in serde_json's canonical form. The retained transcript keeps the raw text the model streamed (translate/reducer.rs:317), the next request re-serializes Claude Code's parsed tool_use.input with sorted keys and no whitespace (translate/request.rs:894), and input_suffix_after_prefix (continuation.rs:532-547) compared the two byte-for-byte. call_id, name, and reasoning items are built identically on both sides, so arguments is the only asymmetric field.

Change

continuation.rs only. input_suffix_after_prefix now compares items through input_items_equivalent: for two FunctionCall items, call_id and name must match and arguments must be byte-equal or parse to equal serde_json::Values; if either side fails to parse and the bytes differ, the items are not equivalent (falls back to the full resend, never to a false continuation). Every other item pair keeps the existing to_value comparison. Nothing recorded or sent upstream changes.

Tests

  • function_call_arguments_with_reordered_keys_keep_continuation — records an Edit call with file_path, old_string, new_string and replays it with sorted keys plus a function_call_output; asserts previous_response_id present and a one-item delta. Fails on main with not_append_only.
  • function_call_arguments_with_different_values_are_not_append_only — a real value change still yields not_append_only.
  • unparseable_function_call_arguments_require_exact_text{not json byte-identical continues; {not json vs {not json does not.
  • input_items_equivalent_compares_arguments_as_json — whitespace-only difference is equivalent; different call_id/name, valid-vs-invalid JSON, and mixed item types are not; Message items compare as before.

cargo fmt --all -- --check clean; cargo test --lib continuation 20 passed; tests/codex_agent_continuation.rs and tests/smoke_cutover.rs pass. No docs/CHANGELOG change (release notes are maintained at release time).

Closes #118

🤖 Generated with Claude Code

@ItsAlbertZhang

Copy link
Copy Markdown
Contributor Author

The Full checks failure is a single clippy::result_large_err at tests/codex_agent_continuation.rs:541 (emit_completion returning Result<(), tungstenite::Error>), a file this PR does not touch: CI's floating stable moved from 1.97.1 (last green run on main, 08-19) to 1.98.0, which newly triggers that lint. I left it out of this branch to keep it to the continuation change; happy to rebase once main is adjusted.

@raine
raine force-pushed the fix/codex-continuation-function-call-arguments branch from 91f60c7 to 635b991 Compare August 23, 2026 11:07
ItsAlbertZhang and others added 3 commits August 23, 2026 14:14
…efix check

The continuation transcript keeps function_call arguments as the raw text
the model streamed, while the next request re-serializes Claude Code's
tool_use input through serde_json (sorted keys, compact). The append-only
prefix check compared the two byte-for-byte, so any tool call whose
arguments the model did not emit in canonical form made the next turn fall
back to a full-context request (not_append_only) and drop
previous_response_id.

Compare FunctionCall items by call_id, name, and parsed argument JSON,
keeping the exact-text comparison as fast path and as fallback when the
arguments do not parse. Other item types are compared as before.

Closes raine#118
clippy 1.98 flags the tungstenite error type returned by emit_completion as
a large Err variant, which fails the lint gate on CI's floating stable
toolchain. handle_socket in the same file already carries this allow for the
same error type, and the helper only writes mock upstream events in a test
harness, so boxing the error buys nothing.
The continuation integration suite only exercised text turns, so the
byte-for-byte function_call argument comparison that broke append-only
continuation after every tool call went unnoticed end to end.

Add a websocket-level test where the mock upstream streams a function_call
whose arguments keep the model's own key order, the client replays the same
call as a tool_use whose canonical serialization sorts the keys, and the next
request must still carry previous_response_id with a single
function_call_output delta. The test fails against the byte-for-byte
comparison and passes against the semantic one.

Also import ResponsesFunctionCallOutput in the continuation unit tests instead
of spelling out its module path at the call site.
@raine
raine force-pushed the fix/codex-continuation-function-call-arguments branch from 635b991 to 63854ee Compare August 23, 2026 11:14
@raine
raine merged commit 0185409 into raine:main Aug 23, 2026
2 checks passed
Digman added a commit to Digman/claude-code-proxy that referenced this pull request Aug 27, 2026
Show full-reset credits and expirations in the monitor, preserve
continuations across semantically equivalent function-call arguments, and
bound empty-completion recovery to one corrective retry.

Upstream: raine#119
bp0lr pushed a commit to bp0lr/claude-code-proxy that referenced this pull request Aug 29, 2026
…efix check (raine#119)

With CCP_CODEX_PREVIOUS_RESPONSE_ID=1, the turn after a tool call fell back to
a full-context request whenever the model did not stream the call's arguments
in serde_json's canonical form. The retained transcript keeps the raw streamed
text, the next request re-serializes the client's parsed tool input with sorted
keys, and the append-only prefix check compared the two byte-for-byte. Losing
previous_response_id also tears down the pooled websocket, so agentic sessions
re-uploaded the whole conversation on most turns.

Compare function_call items by call_id, name, and parsed argument JSON, keeping
the exact-text comparison as fast path and as the fallback when the arguments
do not parse, so an unparseable mismatch still falls back to the full resend.
Nothing recorded or sent upstream changes.

Covered by continuation unit tests and a websocket-level integration test where
the mock upstream streams arguments in the model's own key order while the
client replays their canonical serialization.

Refs raine#118
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants