c18e19c3c7
The api_messages build used a shallow msg.copy(), decoupling only top-level fields. Every nested container (tool_calls entries and their function dicts, multimodal content-part lists, reasoning_details) stayed aliased to the persisted history, so ANY in-place transform on the send copy silently rewrote the stored transcript. Probed every send-path transform against that aliasing shape on main: content strip loop safe (top-level reassign) _canonicalize_api_tool_calls (repair) LEAKED <- #80616's fix _sanitize_messages_surrogates LEAKED (multimodal parts, tc ids/args, reasoning) _sanitize_messages_non_ascii LEAKED (multimodal parts) _sanitize_api_messages safe _drop_thinking_only_and_merge_users safe The retry loop already believed the copies were independent - it sanitizes messages AND api_messages separately (~L3555) - so the aliasing was accidental everywhere. Fix at the chokepoint: _clone_message_for_send clones every container (dict/list) recursively while sharing immutable leaves, so every downstream in-place transform - current and future - is safe by construction. Cost is container-count, not string-bytes: 100KB argument strings and base64 payloads are shared (measured ~0.5ms vs ~0.1ms per 1500-message build; noise next to one json round-trip). Same clone applied to the prefill-message insert (same class, same pipeline). The class-wide invariant test runs the full send-path transform pipeline over an adversarial fixture (malformed args, surrogates, non-ASCII, multimodal parts, reasoning fields) and asserts the history stays byte-identical; an AST contract pins the build-site wiring so the shallow copy can't quietly return. Both mutation-verified: reverting the clone to shallow fails 4 isolation tests, unwiring the build site fails the AST contract. 0xGr1mm's branch fix (previous commit) remains as defense in depth at the exact site the #80498 incident hit; his regression tests and the class-wide invariant give layered coverage.