From cdd3224e46e1586e16e0dc19df889b923865c495 Mon Sep 17 00:00:00 2001 From: Jeff <158072326+jeffrey701@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:13:23 +0200 Subject: [PATCH] fix(llm): count escalation payload_chars as characters, not bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit summarize_messages set EscalationPayloadSummary.payload_chars from String::len() (UTF-8 byte length) rather than the character count the field name means, so any non-ASCII content over-reported ("café" -> 5 instead of 4). server.rs feeds this into security::audit::log_provider_escalation, so the inflated number was written to the operator audit trail for the gated cloud-escalation path. The codebase already treats *_chars metrics as Unicode char counts (dispatch uses chars().count(); telegram has a bytes-vs-chars regression test); escalation.rs was the lone *_chars site on len(). Count with chars().count(); the existing test only used ASCII so the bug was latent. Adds a Unicode regression test. --- crates/genie-core/src/llm/escalation.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/genie-core/src/llm/escalation.rs b/crates/genie-core/src/llm/escalation.rs index aebcca42..5551038b 100644 --- a/crates/genie-core/src/llm/escalation.rs +++ b/crates/genie-core/src/llm/escalation.rs @@ -57,7 +57,7 @@ pub struct EscalationPayloadSummary { pub fn summarize_messages(messages: &[Message]) -> EscalationPayloadSummary { EscalationPayloadSummary { message_count: messages.len(), - payload_chars: messages.iter().map(|m| m.content.len()).sum(), + payload_chars: messages.iter().map(|m| m.content.chars().count()).sum(), } } @@ -116,4 +116,15 @@ mod tests { assert_eq!(summary.message_count, 2); assert_eq!(summary.payload_chars, 6); } + + #[test] + fn summarize_counts_unicode_chars_not_bytes() { + // payload_chars feeds the escalation audit trail, so a multi-byte + // character (café is 4 chars, 5 bytes) must count as one char. + let messages = vec![Message { + role: "user".into(), + content: "café".into(), + }]; + assert_eq!(summarize_messages(&messages).payload_chars, 4); + } }