Rollout trace reducer panics when truncating non-ASCII JSON summaries
What version of Codex CLI is running?
Reproduced with codex-cli 0.142.5. The same implementation is still present in main atbf3c1972b7d045c0a3a48dff91f381070f8f69e1.
What subscription do you have?
Not applicable. This is an offline codex debug trace-reduce failure and does not make a model
request.
Which model were you using?
Not applicable. The failure is deterministic for a raw bundle containing the affected JSON item.
What platform is your computer?
Darwin 25.2.0 arm64 arm (macOS 26.2).
What terminal emulator and version are you using (if applicable)?
Not applicable to the reproducer.
Codex doctor report
Not included because the failure is in the deterministic offline reducer and does not depend on
authentication, model configuration, MCP, network access, or terminal integration.
What issue are you seeing?
codex debug trace-reduce <bundle> can panic while reducing a model-visible JSON item whose
serialized representation is longer than 240 bytes and contains a multi-byte UTF-8 character
across byte offset 240.
The raw trace bundle remains intact (manifest.json, trace.jsonl, and payloads/), but the
command exits unsuccessfully before writing a new state.json. This makes an otherwise complete
raw trace unavailable to consumers of the reduced lifecycle graph.
The failure comes from summarize_json incodex-rs/rollout-trace/src/reducer/conversation/normalize.rs.String::len() returns bytes, while String::truncate() requires its argument to be a UTF-8
character boundary:
if summary.len() > MAX_JSON_SUMMARY_LEN {
summary.truncate(MAX_JSON_SUMMARY_LEN);
summary.push_str("...");
}
What steps can reproduce the bug?
The underlying failure can be reproduced with the same value handled by summarize_json:
use serde_json::json;
let value = json!({"query": "中".repeat(100)});
let mut summary = serde_json::to_string(&value).unwrap();
assert!(summary.len() > 240);
summary.truncate(240); // Panics: byte 240 is not a UTF-8 character boundary.
In the reducer, a parseable function_call.arguments value exercises this path throughraw_text_or_json_body -> json_body -> summarize_json. An emoji payload can trigger the same
failure because common emoji occupy four UTF-8 bytes.
This is data-dependent rather than random: it occurs only when the serialized JSON exceeds 240
bytes and byte offset 240 falls inside a multi-byte code point.
What is the expected behavior?
The reducer should truncate the preview at the closest character boundary at or before the
240-byte budget, append the existing ... marker, and successfully write state.json.
The full JSON should remain available through the existing raw payload reference; only the inline
preview should be truncated.
Additional information
A minimal fix can preserve the current byte budget and marker without changing the trace schema:
let mut truncate_at = MAX_JSON_SUMMARY_LEN;
while !summary.is_char_boundary(truncate_at) {
truncate_at -= 1;
}
summary.truncate(truncate_at);
summary.push_str("...");
Suggested regression coverage:
- Chinese text crossing byte 240;
- emoji crossing byte 240;
- long ASCII JSON preserving the current behavior;
- an input where byte 240 is already a valid character boundary;
- replay through
replay_bundle()while preserving the full raw payload reference.
I would be happy to submit the focused fix and regression tests if a maintainer would like to
invite a pull request, in accordance with the repository contribution policy.