fix(conversation): char-boundary-safe code preview in text_summary

The code-interpreter preview truncated at BYTE 100 — a guaranteed panic
on any CJK/emoji boundary in interpreted code. One poisoned history item
then crashed every subsequent request build on every backend (the
summary feeds both the Messages and ChatCompletions builders). Truncate
at 100 chars via char_indices instead. Panic pinned by test.

Part 2 of the cross-provider replay audit.

Verified: sampling-types 287 tests green.
This commit is contained in:
2026-07-23 00:50:29 -04:00
parent 1fa87566d9
commit 6979407f22
@@ -319,15 +319,15 @@ impl BackendToolCallItem {
format!("[backend x_search] {}({})", ct.name, ct.input) format!("[backend x_search] {}({})", ct.name, ct.input)
} }
BackendToolKind::CodeInterpreter(ci) => { BackendToolKind::CodeInterpreter(ci) => {
// Char-boundary-safe preview: a byte slice (`&c[..100]`)
// panicked on CJK/emoji code, crashing every subsequent
// request build on every backend.
let code_preview = ci let code_preview = ci
.code .code
.as_deref() .as_deref()
.map(|c| { .map(|c| match c.char_indices().nth(100) {
if c.len() > 100 { Some((byte_idx, _)) => format!("{}...", &c[..byte_idx]),
format!("{}...", &c[..100]) None => c.to_string(),
} else {
c.to_string()
}
}) })
.unwrap_or_default(); .unwrap_or_default();
format!("[backend code_interpreter] {code_preview}") format!("[backend code_interpreter] {code_preview}")
@@ -9623,6 +9623,41 @@ mod tests {
/// raw_output decompose-vs-passthrough split -- they are always /// raw_output decompose-vs-passthrough split -- they are always
/// passed through inline. We test that all BackendToolCall items /// passed through inline. We test that all BackendToolCall items
/// survive serialization at their position.) /// survive serialization at their position.)
#[test]
/// `text_summary`'s code preview truncated at BYTE 100 (`&c[..100]`) —
/// a panic on any non-ASCII boundary (CJK/emoji in interpreted code).
/// One poisoned history item then crashed every subsequent request
/// build on every backend. Truncation must be char-boundary safe.
#[test]
fn code_interpreter_summary_truncates_multibyte_code_safely() {
let item = BackendToolCallItem {
kind: BackendToolKind::CodeInterpreter(rs::CodeInterpreterToolCall {
code: Some("统计".repeat(40)),
container_id: "cont_1".to_string(),
id: "ci_1".to_string(),
outputs: None,
status: rs::CodeInterpreterToolCallStatus::Completed,
}),
};
let summary = item.text_summary();
assert!(summary.starts_with("[backend code_interpreter] 统计"));
assert!(
summary.ends_with("..."),
"long code must truncate: {summary}"
);
// ASCII shorter than the cap stays whole.
let short = BackendToolCallItem {
kind: BackendToolKind::CodeInterpreter(rs::CodeInterpreterToolCall {
code: Some("print(1)".to_string()),
container_id: "cont_1".to_string(),
id: "ci_2".to_string(),
outputs: None,
status: rs::CodeInterpreterToolCallStatus::Completed,
}),
};
assert_eq!(short.text_summary(), "[backend code_interpreter] print(1)");
}
#[test] #[test]
fn backend_tool_call_position_stable() { fn backend_tool_call_position_stable() {
let ws_a = ConversationItem::BackendToolCall(BackendToolCallItem { let ws_a = ConversationItem::BackendToolCall(BackendToolCallItem {