diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index b0720896c6..56a81e88ec 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1845,9 +1845,9 @@ "quick-xml_0.39.4": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\">=0.4, <0.9\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\">=1.0.180\"},{\"kind\":\"dev\",\"name\":\"serde-value\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.206\"},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"async-tokio\":[\"tokio\"],\"default\":[],\"encoding\":[\"encoding_rs\"],\"escape-html\":[],\"overlapped-lists\":[],\"serde-types\":[\"serde/derive\"],\"serialize\":[\"serde\"]}}", "quick-xml_0.41.0": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\">=0.4, <0.9\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\">=1.0.180\"},{\"kind\":\"dev\",\"name\":\"serde-value\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.206\"},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"async-tokio\":[\"tokio\"],\"default\":[],\"encoding\":[\"encoding_rs\"],\"escape-html\":[],\"overlapped-lists\":[],\"serde-types\":[\"serde/derive\"],\"serialize\":[\"serde\"]}}", "quickcheck_1.1.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"env_logger\",\"optional\":true,\"req\":\"^0.11\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"sys_rng\"],\"name\":\"rand\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"regex\",\"use_logging\"],\"regex\":[\"env_logger/regex\"],\"use_logging\":[\"log\",\"env_logger\"]}}", - "quinn-proto_0.11.18": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.1\"},{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.9\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"fastbloom\",\"optional\":true,\"req\":\"^0.17\"},{\"default_features\":false,\"features\":[\"wasm_js\"],\"name\":\"getrandom\",\"req\":\"^0.4\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"lru-slab\",\"req\":\"^0.1.2\"},{\"name\":\"qlog\",\"optional\":true,\"req\":\"^0.15.2\"},{\"name\":\"rand\",\"req\":\"^0.10.1\"},{\"name\":\"rand_pcg\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"features\":[\"wasm32_unknown_unknown_js\"],\"name\":\"ring\",\"req\":\"^0.17\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"name\":\"rustc-hash\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.5\"},{\"features\":[\"web\"],\"name\":\"rustls-pki-types\",\"req\":\"^1.7\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"slab\",\"req\":\"^0.4.6\"},{\"name\":\"thiserror\",\"req\":\"^2.0.3\"},{\"features\":[\"alloc\",\"alloc\"],\"name\":\"tinyvec\",\"req\":\"^1.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.10\"},{\"default_features\":false,\"features\":[\"env-filter\",\"fmt\",\"ansi\",\"time\",\"local-time\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.45\"},{\"name\":\"web-time\",\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"}],\"features\":{\"__rustls-post-quantum-test\":[],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs?/aws-lc-sys\",\"aws-lc-rs?/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"aws-lc-rs\",\"aws-lc-rs?/fips\"],\"bloom\":[\"dep:fastbloom\"],\"default\":[\"rustls-ring\",\"log\",\"bloom\"],\"log\":[\"tracing/log\"],\"platform-verifier\":[\"dep:rustls-platform-verifier\"],\"qlog\":[\"dep:qlog\"],\"ring\":[\"dep:ring\"],\"rustls\":[\"rustls-ring\"],\"rustls-aws-lc-rs\":[\"dep:rustls\",\"rustls?/aws-lc-rs\",\"aws-lc-rs\"],\"rustls-aws-lc-rs-fips\":[\"rustls-aws-lc-rs\",\"aws-lc-rs-fips\"],\"rustls-log\":[\"rustls?/logging\"],\"rustls-ring\":[\"dep:rustls\",\"rustls?/ring\",\"ring\"]}}", - "quinn-udp_0.5.14": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"name\":\"libc\",\"req\":\"^0.2.158\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"once_cell\",\"req\":\"^1.19\",\"target\":\"cfg(windows)\"},{\"name\":\"socket2\",\"req\":\">=0.5, <0.7\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"features\":[\"sync\",\"rt\",\"rt-multi-thread\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.28.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.10\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_IO\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.60\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"tracing\",\"log\"],\"direct-log\":[\"dep:log\"],\"fast-apple-datapath\":[],\"log\":[\"tracing/log\"]}}", - "quinn-udp_0.6.1": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\"},{\"name\":\"libc\",\"req\":\"^0.2.175\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"socket2\",\"req\":\">=0.5, <0.7\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"features\":[\"sync\",\"rt\",\"rt-multi-thread\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.28.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.10\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_IO\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"tracing\",\"tracing-log\"],\"fast-apple-datapath\":[],\"log\":[\"dep:log\"],\"tracing-log\":[\"tracing/log\"]}}", + "quinn-proto_0.11.19": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.1\"},{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.9\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"fastbloom\",\"optional\":true,\"req\":\"^0.17\"},{\"default_features\":false,\"features\":[\"wasm_js\"],\"name\":\"getrandom\",\"req\":\"^0.4\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"lru-slab\",\"req\":\"^0.1.2\"},{\"name\":\"qlog\",\"optional\":true,\"req\":\"^0.15.2\"},{\"name\":\"rand\",\"req\":\"^0.10.1\"},{\"name\":\"rand_pcg\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"features\":[\"wasm32_unknown_unknown_js\"],\"name\":\"ring\",\"req\":\"^0.17\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"name\":\"rustc-hash\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.5\"},{\"features\":[\"web\"],\"name\":\"rustls-pki-types\",\"req\":\"^1.7\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"slab\",\"req\":\"^0.4.6\"},{\"name\":\"thiserror\",\"req\":\"^2.0.3\"},{\"features\":[\"alloc\",\"alloc\"],\"name\":\"tinyvec\",\"req\":\"^1.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.10\"},{\"default_features\":false,\"features\":[\"env-filter\",\"fmt\",\"ansi\",\"time\",\"local-time\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.45\"},{\"name\":\"web-time\",\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"}],\"features\":{\"__rustls-post-quantum-test\":[],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs?/aws-lc-sys\",\"aws-lc-rs?/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"aws-lc-rs\",\"aws-lc-rs?/fips\"],\"bloom\":[\"dep:fastbloom\"],\"default\":[\"rustls-ring\",\"log\",\"bloom\"],\"log\":[\"tracing/log\"],\"platform-verifier\":[\"dep:rustls-platform-verifier\"],\"qlog\":[\"dep:qlog\"],\"ring\":[\"dep:ring\"],\"rustls\":[\"rustls-ring\"],\"rustls-aws-lc-rs\":[\"dep:rustls\",\"rustls?/aws-lc-rs\",\"aws-lc-rs\"],\"rustls-aws-lc-rs-fips\":[\"rustls-aws-lc-rs\",\"aws-lc-rs-fips\"],\"rustls-log\":[\"rustls?/logging\"],\"rustls-ring\":[\"dep:rustls\",\"rustls?/ring\",\"ring\"]}}", + "quinn-udp_0.5.16": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"name\":\"libc\",\"req\":\"^0.2.158\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"once_cell\",\"req\":\"^1.19\",\"target\":\"cfg(windows)\"},{\"name\":\"socket2\",\"req\":\">=0.5, <0.7\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"features\":[\"sync\",\"rt\",\"rt-multi-thread\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.28.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.10\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_IO\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"tracing\",\"log\"],\"direct-log\":[\"dep:log\"],\"fast-apple-datapath\":[],\"log\":[\"tracing/log\"]}}", + "quinn-udp_0.6.3": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"name\":\"libc\",\"req\":\"^0.2.175\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"socket2\",\"req\":\">=0.5, <0.7\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"features\":[\"sync\",\"rt\",\"rt-multi-thread\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.28.1\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.10\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_IO\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"tracing\",\"tracing-log\"],\"fast-apple-datapath\":[],\"log\":[\"dep:log\"],\"tracing-log\":[\"tracing/log\"]}}", "quinn_0.11.9": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.22\"},{\"name\":\"async-io\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"async-std\",\"optional\":true,\"req\":\"^1.11\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"crc\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"directories-next\",\"req\":\"^2\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.19\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"proto\",\"package\":\"quinn-proto\",\"req\":\"^0.11.12\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"name\":\"rustc-hash\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.5\"},{\"kind\":\"dev\",\"name\":\"rustls-pemfile\",\"req\":\"^2\"},{\"name\":\"smol\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"socket2\",\"req\":\">=0.5, <0.7\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"thiserror\",\"req\":\"^2.0.3\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.28.1\"},{\"features\":[\"sync\",\"rt\",\"rt-multi-thread\",\"time\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.28.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.10\"},{\"default_features\":false,\"features\":[\"std-future\"],\"kind\":\"dev\",\"name\":\"tracing-futures\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"features\":[\"env-filter\",\"fmt\",\"ansi\",\"time\",\"local-time\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"default_features\":false,\"features\":[\"tracing\"],\"name\":\"udp\",\"package\":\"quinn-udp\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2\"},{\"name\":\"web-time\",\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"}],\"features\":{\"aws-lc-rs\":[\"proto/aws-lc-rs\"],\"aws-lc-rs-fips\":[\"proto/aws-lc-rs-fips\"],\"bloom\":[\"proto/bloom\"],\"default\":[\"log\",\"platform-verifier\",\"runtime-tokio\",\"rustls-ring\",\"bloom\"],\"lock_tracking\":[],\"log\":[\"tracing/log\",\"proto/log\",\"udp/log\"],\"platform-verifier\":[\"proto/platform-verifier\"],\"qlog\":[\"proto/qlog\"],\"ring\":[\"proto/ring\"],\"runtime-async-std\":[\"async-io\",\"async-std\"],\"runtime-smol\":[\"async-io\",\"smol\"],\"runtime-tokio\":[\"tokio/time\",\"tokio/rt\",\"tokio/net\"],\"rustls\":[\"rustls-ring\"],\"rustls-aws-lc-rs\":[\"dep:rustls\",\"aws-lc-rs\",\"proto/rustls-aws-lc-rs\",\"proto/aws-lc-rs\"],\"rustls-aws-lc-rs-fips\":[\"dep:rustls\",\"aws-lc-rs-fips\",\"proto/rustls-aws-lc-rs-fips\",\"proto/aws-lc-rs-fips\"],\"rustls-log\":[\"rustls?/logging\"],\"rustls-ring\":[\"dep:rustls\",\"ring\",\"proto/rustls-ring\",\"proto/ring\"]}}", "quote_1.0.45": "{\"dependencies\":[{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.80\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{\"default\":[\"proc-macro\"],\"proc-macro\":[\"proc-macro2/proc-macro\"]}}", "r-efi_5.3.0": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"efiapi\":[],\"examples\":[\"native\"],\"native\":[],\"rustc-dep-of-std\":[\"core\"]}}", diff --git a/codex-rs/.cargo/audit.toml b/codex-rs/.cargo/audit.toml index f42aafa441..db2deb24d1 100644 --- a/codex-rs/.cargo/audit.toml +++ b/codex-rs/.cargo/audit.toml @@ -24,6 +24,10 @@ ignore = [ "RUSTSEC-2026-0511", # hickory-proto 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2 "RUSTSEC-2026-0512", # hickory-proto 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2 "RUSTSEC-2026-0514", # hickory-resolver 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2 + # Additional DNSSEC-only advisories reviewed 2026-11-13. + "RUSTSEC-2026-0530", # hickory-proto 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2 + "RUSTSEC-2026-0531", # hickory-proto 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2 + "RUSTSEC-2026-0532", # hickory-proto 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2 "RUSTSEC-2026-0194", # quick-xml via plist/syntect and wayland-scanner; trusted inputs only; remove when rust-plist#191 and wayland-rs#938 are released "RUSTSEC-2026-0195", # quick-xml via plist/syntect and wayland-scanner; trusted inputs only; remove when rust-plist#191 and wayland-rs#938 are released "RUSTSEC-2026-0370", # zvariant 4.2.0 via zbus 4.4.0 in Linux TUI and keyring/secret-service; remove when those paths migrate off the retired standalone crates diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index afdb72ad7f..af0c8502b1 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -12571,7 +12571,7 @@ dependencies = [ "futures-io", "pin-project-lite", "quinn-proto", - "quinn-udp 0.5.14", + "quinn-udp 0.5.16", "rustc-hash", "rustls", "socket2 0.6.3", @@ -12583,9 +12583,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.18" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" dependencies = [ "bytes", "getrandom 0.4.2", @@ -12605,29 +12605,29 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" dependencies = [ "cfg_aliases 0.2.1", "libc", "once_cell", "socket2 0.6.3", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] name = "quinn-udp" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76150b617afc75e6e21ac5f39bc196e80b65415ae48d62dbef8e2519d040ce42" +checksum = "47091c86514c2e11452cb228dee77037334b99d92a23e4c69aa5982261f39247" dependencies = [ "cfg_aliases 0.2.1", "libc", "log", "socket2 0.6.3", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -17315,7 +17315,7 @@ dependencies = [ "event-listener", "futures", "log", - "quinn-udp 0.6.1", + "quinn-udp 0.6.3", "rtc", "tokio", ] diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index b08be3e4b0..c4ed2837ec 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index e4522657cd..c51fc93873 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -18,6 +18,7 @@ use codex_protocol::protocol::TurnEnvironmentRequests; use codex_protocol::protocol::TurnSettingsUpdate; use codex_protocol::protocol::TurnSettingsUpdateOutcome; use codex_protocol::turn_input::AnnotatedResponseItem; +use codex_protocol::turn_input::InjectedResponseItem; use codex_protocol::turn_input::ResponseItemAnnotations; use codex_utils_path_uri::PathUri; @@ -1081,15 +1082,17 @@ impl TurnRequestProcessor { .into_iter() .enumerate() .map(|(index, value)| { - serde_json::from_value::(value) + serde_json::from_value::(value) .map_err(|err| format!("items[{index}] is not a valid response item: {err}")) }) .collect::, _>>() .map_err(invalid_request)?; - validate_response_item_image_urls(&items)?; + for item in &items { + validate_response_item_image_urls(std::slice::from_ref(&item.item))?; + } thread - .inject_response_items(items) + .inject_response_items_with_retention(items) .await .map_err(|err| match err.details() { CodexErrorDetails::InvalidRequest(message) => invalid_request(message.clone()), diff --git a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs index 384137b95a..6ddee6477a 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs @@ -127,9 +127,9 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( thread_id: thread.id.clone(), items: vec![ serde_json::to_value(&injected_item)?, - serde_json::to_value(&injected_developer_item)?, - serde_json::to_value(&marker_shaped_developer_item)?, - named_tool_output.clone(), + json!({"item": injected_developer_item, "retention": "default"}), + json!({"item": marker_shaped_developer_item, "retention": "always"}), + json!({"item": named_tool_output, "retention": "always"}), ], }) .await?; @@ -160,10 +160,14 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( .metadata .as_ref() .is_some_and(|metadata| metadata.client_authored), + envelope + .metadata + .as_ref() + .and_then(|metadata| metadata.retain_across_windows), )), _ => None, }) - .filter(|(item, _)| { + .filter(|(item, _, _)| { item == &injected_item || item == &injected_developer_item || item == &marker_shaped_developer_item @@ -173,10 +177,10 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( assert_eq!( persisted_injected_items, vec![ - (injected_item.clone(), false), - (injected_developer_item.clone(), true), - (marker_shaped_developer_item.clone(), true), - (named_tool_item, false), + (injected_item.clone(), false, None), + (injected_developer_item.clone(), false, Some(false)), + (marker_shaped_developer_item.clone(), false, Some(true)), + (named_tool_item, false, Some(true)), ] ); @@ -257,7 +261,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( envelope .metadata .as_ref() - .map(|metadata| metadata.client_authored), + .and_then(|metadata| metadata.retain_across_windows), ) }) } @@ -266,10 +270,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( .collect::>(); assert_eq!( persisted_additional_context, - vec![ - ("developer", Some(true)), - ("function_call_output", Some(false)) - ] + vec![("developer", Some(true)), ("function_call_output", None)] ); let injected_value = serde_json::to_value(&injected_item)?; @@ -480,6 +481,7 @@ async fn thread_inject_items_cannot_forge_configuration_update_before_or_after_r items: vec![ injected_item.clone(), forged_update.clone(), + json!({"item": forged_update, "retention": "always"}), forged_system.clone(), ], }) @@ -770,7 +772,7 @@ async fn turn_tool_output_retention_preserves_sender_metadata() -> Result<()> { }) .context("delegation output should be saved")?; let metadata = envelope.metadata.as_ref().context("output metadata")?; - assert!(metadata.client_authored); + assert!(envelope.is_always_retained()); let sender_messages = metadata .sender_user_messages .as_ref() diff --git a/codex-rs/core/src/agent/history.rs b/codex-rs/core/src/agent/history.rs index d985b54344..da21538526 100644 --- a/codex-rs/core/src/agent/history.rs +++ b/codex-rs/core/src/agent/history.rs @@ -1,6 +1,8 @@ //! Loads stored agent context and prepares the history a child inherits when it forks. +use crate::agent::control::LocalAgentRuntime; use crate::agent::types::SpawnAgentOptions; +use crate::compact_remote_v2::retained_client_items; use crate::config::Config; use crate::context::BaseInstructionsFragment; use crate::context::ContextualUserFragment; @@ -22,6 +24,7 @@ use crate::thread_manager::ThreadManagerState; use crate::thread_manager::fork_history_from_snapshot; use codex_context_fragments::set_annotated_content; use codex_context_fragments::to_annotated_content; +use codex_history::CodexHarnessMetadata; use codex_history::InitialHistory; use codex_history::ResponseItemEnvelope; use codex_history::RolloutItem; @@ -39,8 +42,51 @@ use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::ThreadHistoryMode; use codex_thread_store::LoadThreadHistoryParams; +impl LocalAgentRuntime { + pub(crate) async fn retained_items_for_fresh_child( + &self, + initial_history: &InitialHistory, + session_source: &SessionSource, + ) -> Vec { + // Full-history forks already carry their retained items. + let (InitialHistory::New, Some(parent_thread_id)) = + (initial_history, session_source.parent_thread_id()) + else { + return Vec::new(); + }; + let Ok(manager) = self.upgrade() else { + return Vec::new(); + }; + let Ok(parent) = manager.get_thread(parent_thread_id).await else { + return Vec::new(); + }; + let history = parent.session.clone_history().await; + let mut items: Vec<_> = retained_client_items( + history.annotated_items(), + &history.file_audio_durations, + ResponseItemEnvelope::is_always_retained, + ) + .into_values() + .collect(); + for item in &mut items { + let parent_metadata = item.metadata.take().unwrap_or_default(); + // Keep item metadata needed for retention and replay. Checkpoints belong to the child. + item.metadata = Some(CodexHarnessMetadata { + retain_across_windows: parent_metadata.retain_across_windows, + retained_source: parent_metadata.retained_source, + history_truncation_token_limit: parent_metadata.history_truncation_token_limit, + image_replay_sources: parent_metadata.image_replay_sources, + inherited_user_message: true, + ..Default::default() + }); + } + items + } +} + fn keep_forked_rollout_item(item: &RolloutItem, preserve_context_baselines: bool) -> bool { match item { + RolloutItem::ResponseItem(envelope) if envelope.is_always_retained() => true, RolloutItem::ResponseItem(envelope) => match &envelope.item { ResponseItem::Message { role, phase, .. } => match role.as_str() { "system" | "developer" | "user" => true, @@ -256,9 +302,11 @@ pub(super) async fn prepare_forked_history( forked_rollout_items = items; } let retain_forked_item = |envelope: &mut ResponseItemEnvelope, replaced: &mut bool| { - if multi_agent_version == MultiAgentVersion::V2 - && (preserve_fork_prefix - || matches!(&envelope.item, ResponseItem::Message { role, .. } if role == "user" || role == "assistant")) + let explicitly_retained = envelope.is_always_retained(); + if explicitly_retained + || (multi_agent_version == MultiAgentVersion::V2 + && (preserve_fork_prefix + || matches!(&envelope.item, ResponseItem::Message { role, .. } if role == "user" || role == "assistant"))) { // Resume must not recapture copied parent messages as local authorization. envelope @@ -273,7 +321,7 @@ pub(super) async fn prepare_forked_history( // Assistant and tool positions belong to the parent counter, not the child. metadata.user_input_order = None; } - if preserve_fork_prefix { + if preserve_fork_prefix || explicitly_retained { return true; } let response_item = &mut envelope.item; diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 9222dff46e..6540c74665 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -62,6 +62,8 @@ use codex_protocol::protocol::TurnEnvironmentRequests; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::protocol::W3cTraceContext; +use codex_protocol::turn_input::InjectedResponseItem; +use codex_protocol::turn_input::ItemRetention; use codex_protocol::turn_input::RecoverTurnRequest; use codex_protocol::turn_input::StartIfIdleSubmission; use codex_protocol::turn_input::SteerSubmission; @@ -1032,7 +1034,15 @@ impl CodexThread { /// Record raw Responses API items without starting a new turn. pub async fn inject_response_items(&self, items: Vec) -> CodexResult<()> { - self.inject_response_items_for_turn(items).await?; + self.inject_response_items_with_retention(items).await + } + + /// Inject Responses items with explicit, persisted retention policies. + pub async fn inject_response_items_with_retention( + &self, + items: Vec>, + ) -> CodexResult<()> { + self.record_injected_items(items).await?; self.checkpoint_preparation().await?; Ok(()) } @@ -1058,37 +1068,43 @@ impl CodexThread { pub async fn inject_response_items_for_turn( &self, items: Vec, + ) -> CodexResult<()> { + self.record_injected_items(items).await + } + + async fn record_injected_items( + &self, + items: Vec>, ) -> CodexResult<()> { if items.is_empty() { return Err(CodexErr::InvalidRequest( "items must not be empty".to_string(), )); } - items.iter().try_for_each(validate_injected_response_item)?; - - let had_reference_context = self.session.reference_context_item().await.is_some(); - let mut turn_context = if had_reference_context { - self.session.new_inject_items_context().await - } else { - self.session.new_default_turn().await - }; - if self.session.reference_context_item().await.is_none() { - // Compaction can clear the reference while the recording context is built. - // Initial context must capture a step with a complete skills snapshot. - if had_reference_context { - turn_context = self.session.new_default_turn().await; + let items: Vec = items.into_iter().map(Into::into).collect(); + let mut indexed_items = items.iter().enumerate(); + while let Some((index, item)) = indexed_items.next() { + validate_injected_response_item(&item.item)?; + if item.retention != ItemRetention::Always { + continue; + } + let needs_call = crate::tools::output_call_id(&item.item).is_some() + && !matches!(&item.item, ResponseItem::ToolSearchOutput { execution, .. } if execution == "server"); + if (crate::tools::input_call_id(&item.item).is_some() || needs_call) + && !indexed_items.next().is_some_and(|(_, output)| { + output.retention == ItemRetention::Always + && crate::tools::is_matching_tool_output(&item.item, &output.item) + }) + { + return Err(CodexErr::InvalidRequest(format!( + "items[{index}]: retained tool calls and outputs must be adjacent in the same request, call first, with both marked always" + ))); } - // This history-only API runs without run_turn, so it owns its initial step. - let step_context = self - .session - .capture_step_context(Arc::clone(&turn_context), &CancellationToken::new()) - .await?; - self.session - .record_context_updates_and_set_reference_context_item(step_context.as_ref()) - .await?; } + + let turn_context = self.session.history_injection_context().await?; self.session - .inject_client_response_items(items, turn_context.as_ref()) + .inject_client_response_items(items, &turn_context) .await; Ok(()) } diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 41ea9ddd38..b980c8ff88 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -7,12 +7,14 @@ use crate::Prompt; use crate::client::ModelClientSession; use crate::client_common::ResponseEvent; use crate::compact_pending_input::is_retained_pending_mail; +use crate::compact_remote_v2::retained_client_items; use crate::context::BaseInstructionsFragment; use crate::context::CompactionSummary; use crate::context::ContextualUserFragment; use crate::context::world_state::WorldState; use crate::context::world_state::WorldStateSnapshot; use crate::context::world_state::split_prefix_updates; +use crate::context_manager::ContextManager; use crate::context_manager::updates::merge_world_state_updates; use crate::hook_runtime::PostCompactHookOutcome; use crate::hook_runtime::PreCompactHookOutcome; @@ -20,6 +22,7 @@ use crate::hook_runtime::run_post_compact_hooks; use crate::hook_runtime::run_pre_compact_hooks; use crate::responses_metadata::CodexResponsesMetadata; use crate::responses_metadata::CompactionTurnMetadata; +use crate::session::FileAudioDurations; use crate::session::RequestEffortUsage; use crate::session::session::Session; use crate::session::step_context::StepContext; @@ -379,7 +382,6 @@ async fn run_compact_task_inner_impl( }; let history_snapshot = sess.clone_history().await; - let history_items = history_snapshot.annotated_items(); let summary_suffix = if matches!(compaction_metadata.phase(), CompactionPhase::PostTurn) { get_last_assistant_message_from_turn(compaction_response.output.iter()) .filter(|summary| !summary.trim().is_empty()) @@ -392,9 +394,8 @@ async fn run_compact_task_inner_impl( get_last_assistant_message_from_turn(history_snapshot.raw_items()).unwrap_or_default() }; let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}"); - let user_messages = collect_annotated_user_messages(history_items); - let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text); + let mut new_history = build_compacted_history(&history_snapshot, &summary_text); if let Some(summary_item) = new_history.last_mut() { // This replacement history skips `record_conversation_items`; only the appended summary // belongs to this compaction turn. @@ -407,6 +408,9 @@ async fn run_compact_task_inner_impl( let summary = new_history .pop() .ok_or_else(|| CodexErr::Fatal("local compaction summary is missing".to_string()))?; + // A client may also have explicitly retained this mail. Use the final + // compactor input for pending mail so each delivery appears once. + new_history.retain(|item| !is_retained_pending_mail(item, first)); new_history.extend( history .annotated_items() @@ -606,13 +610,13 @@ pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec Vec> { - items - .iter() - .filter_map(|envelope| compacted_user_message(&envelope.item, envelope.metadata.as_ref())) - .collect() +) -> impl DoubleEndedIterator)> { + items.iter().enumerate().filter_map(|(position, envelope)| { + let message = compacted_user_message(&envelope.item, envelope.metadata.as_ref())?; + Some((position, message)) + }) } fn compacted_user_message<'a>( @@ -714,28 +718,33 @@ pub(crate) fn insert_initial_context_before_last_real_user_or_summary( } pub(crate) fn build_compacted_history( - initial_context: Vec, - user_messages: &[CompactedUserMessage<'_>], + history: &ContextManager, summary_text: &str, ) -> Vec { build_compacted_history_with_limit( - initial_context, - user_messages, + history.annotated_items(), summary_text, COMPACT_USER_MESSAGE_MAX_TOKENS, + &history.file_audio_durations, ) } fn build_compacted_history_with_limit( - mut history: Vec, - user_messages: &[CompactedUserMessage<'_>], + items: &[ResponseItemEnvelope], summary_text: &str, max_tokens: usize, + audio_durations: &FileAudioDurations, ) -> Vec { - let mut selected_messages = Vec::new(); + // An injected user message can qualify under both budgets. Its position + // identifies the occurrence even if another message has the same client ID. + let mut selected = retained_client_items( + items, + audio_durations, + ResponseItemEnvelope::is_always_retained, + ); if max_tokens > 0 { let mut remaining = max_tokens; - for message in user_messages.iter().rev() { + for (position, message) in annotated_user_messages(items).rev() { if remaining == 0 { break; } @@ -749,6 +758,10 @@ fn build_compacted_history_with_limit( else { continue; }; + if selected.contains_key(&position) { + remaining = remaining.saturating_sub(tokens); + continue; + } let mut harness_metadata = message.harness_metadata.cloned(); let preserve_content = tokens <= remaining && content @@ -806,18 +819,17 @@ fn build_compacted_history_with_limit( { metadata.mark_retained_sources_incomplete(); } - selected_messages.push(ResponseItemEnvelope { - item, - metadata: harness_metadata, - }); - if tokens > remaining { - break; - } + selected.insert( + position, + ResponseItemEnvelope { + item, + metadata: harness_metadata, + }, + ); remaining = remaining.saturating_sub(tokens); } - selected_messages.reverse(); } - history.extend(selected_messages); + let mut history: Vec<_> = selected.into_values().collect(); let summary_text = if summary_text.is_empty() { "(no summary available)".to_string() diff --git a/codex-rs/core/src/compact_remote_history.rs b/codex-rs/core/src/compact_remote_history.rs index 3a6f1500f3..25321d6464 100644 --- a/codex-rs/core/src/compact_remote_history.rs +++ b/codex-rs/core/src/compact_remote_history.rs @@ -1,13 +1,13 @@ -use std::borrow::Borrow; +use std::collections::BTreeMap; -use crate::compact_retained_tool_outputs::is_retained_tool_output; +use crate::compact_remote_v2::should_retain_client_item; use crate::context::ContextualUserFragment; use crate::context::ImageResizeNotice; use crate::context_manager::ContextManager; use crate::context_manager::estimate_item_token_count_with_audio; use crate::session::FileAudioDurations; use crate::session::turn_context::TurnContext; -use codex_features::Feature; +use crate::tools::is_matching_tool_output; use codex_history::ResponseItemEnvelope; use codex_protocol::models::BaseInstructions; use codex_protocol::models::ContentItem; @@ -19,51 +19,85 @@ use codex_utils_output_truncation::approx_token_count; const CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE: &str = "Output exceeded the available model context and was truncated"; -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct HistoryItemGroup { - pub(crate) source: T, - pub(crate) attached_notice: Option, +pub(crate) struct HistoryItemGroup<'a> { + pub(crate) source_position: usize, + pub(crate) source: &'a ResponseItemEnvelope, + pub(crate) attached_notice: Option<&'a ResponseItemEnvelope>, + pub(crate) tool_call: Option<&'a ResponseItemEnvelope>, } -impl> HistoryItemGroup { - pub(crate) fn into_items(self) -> impl Iterator { - std::iter::once(self.source).chain(self.attached_notice) +impl HistoryItemGroup<'_> { + pub(crate) fn keep( + &self, + selected: &mut BTreeMap, + source: ResponseItemEnvelope, + ) { + if let Some(call) = self.tool_call { + selected.insert(self.source_position - 1, call.clone()); + } + selected.insert(self.source_position, source); + if let Some(notice) = self.attached_notice { + selected.insert(self.source_position + 1, notice.clone()); + } } pub(crate) fn estimated_token_count(&self, audio_durations: &FileAudioDurations) -> i128 { - let source_tokens = i128::from(estimate_item_token_count_with_audio( - self.source.borrow(), - audio_durations, - )); - let notice_tokens = self.attached_notice.as_ref().map_or(0, |notice| { - i128::from(estimate_item_token_count_with_audio( - notice.borrow(), - audio_durations, - )) - }); - source_tokens.saturating_add(notice_tokens) + self.tool_call + .into_iter() + .chain(std::iter::once(self.source)) + .chain(self.attached_notice) + .map(|item| i128::from(estimate_item_token_count_with_audio(item, audio_durations))) + .fold(0, i128::saturating_add) } } -pub(crate) fn history_item_groups(items: I) -> impl Iterator> -where - I: IntoIterator, - I::Item: Borrow, -{ - let mut items = items.into_iter().peekable(); +pub(crate) fn newest_history_item_groups( + mut items: &[ResponseItemEnvelope], +) -> impl Iterator> { std::iter::from_fn(move || { - let source = items.next()?; - let attached_notice = items.next_if(|notice| is_attached_notice(notice.borrow())); + let (last, preceding) = items.split_last()?; + let (source, attached_notice, rest) = match preceding.split_last() { + Some((source, rest)) if is_attached_notice(source, last) => (source, Some(last), rest), + _ => (last, None, preceding), + }; + let source_position = rest.len(); + // Injection requires explicitly retained exchanges to be adjacent in the request. + let (tool_call, rest) = match rest.split_last() { + Some((call, rest)) + if source.is_always_retained() + && call.is_always_retained() + && is_matching_tool_output(&call.item, &source.item) => + { + (Some(call), rest) + } + _ => (None, rest), + }; + items = rest; Some(HistoryItemGroup { + source_position, source, attached_notice, + tool_call, }) }) } -fn is_attached_notice(notice: &ResponseItem) -> bool { +fn is_attached_notice(source: &ResponseItemEnvelope, notice: &ResponseItemEnvelope) -> bool { + let can_have_resize_notice = match &source.item { + ResponseItem::Message { role, .. } => role == "user", + ResponseItem::FunctionCallOutput { .. } | ResponseItem::CustomToolCallOutput { .. } => true, + _ => false, + }; + // A client may send the same text as a resize notice with its own policy. + if !can_have_resize_notice + || notice.metadata.as_ref().is_some_and(|metadata| { + metadata.client_authored || metadata.retain_across_windows.is_some() + }) + { + return false; + } matches!( - notice, + ¬ice.item, ResponseItem::Message { role, content, .. } if role == "developer" && matches!( @@ -89,38 +123,24 @@ pub(crate) fn trim_function_call_history_to_fit_context_window( i128::try_from(approx_token_count(&base_instructions.text)).unwrap_or(i128::MAX); let audio_durations = &history.file_audio_durations; let original_items = history.annotated_items(); - let mut estimated_tokens = history_item_groups(original_items.iter().map(|item| &item.item)) - .map(|group| group.estimated_token_count(audio_durations)) + let mut estimated_tokens = original_items + .iter() + .map(|item| i128::from(estimate_item_token_count_with_audio(item, audio_durations))) .fold(base_tokens, i128::saturating_add); let initial_estimated_tokens = i64::try_from(estimated_tokens).unwrap_or(i64::MAX); + if initial_estimated_tokens <= context_window { + return (0, 0); + } let mut rewritten_items = Vec::new(); - let mut consumed_items: usize = 0; - - for group in history_item_groups(original_items.iter().map(|item| &item.item)) - .collect::>() - .into_iter() - .rev() - { + let mut retained_len = original_items.len(); + for group in newest_history_item_groups(original_items) { if i64::try_from(estimated_tokens).unwrap_or(i64::MAX) <= context_window { break; } - let group_item_count = 1 + usize::from(group.attached_notice.is_some()); - let source_index = original_items - .len() - .saturating_sub(consumed_items.saturating_add(group_item_count)); - let Some(rewritten_item) = original_items.get(source_index).and_then(|envelope| { - if turn_context - .config - .features - .enabled(Feature::RetainClientToolOutputs) - && is_retained_tool_output(envelope) - { - // Enabled retained outputs form the same boundary as user/developer messages. - None - } else { - rewritten_output_for_context_window(envelope) - } - }) else { + if should_retain_client_item(group.source, &turn_context.config.features) { + break; + } + let Some(rewritten_item) = rewritten_output_for_context_window(group.source) else { break; }; estimated_tokens = estimated_tokens @@ -129,13 +149,12 @@ pub(crate) fn trim_function_call_history_to_fit_context_window( &rewritten_item.item, audio_durations, ))); - consumed_items += group_item_count; + retained_len = group.source_position; rewritten_items.push(rewritten_item); } let rewritten_outputs = rewritten_items.len(); if rewritten_outputs > 0 { - let retained_len = original_items.len() - consumed_items; let mut items = original_items[..retained_len].to_vec(); items.extend(rewritten_items.into_iter().rev()); history.replace_annotated(items); diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index e2c6de0357..6d9f535b5c 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -1,4 +1,5 @@ use codex_login::AuthOwner; +use std::collections::BTreeMap; use std::sync::Arc; use std::sync::OnceLock; @@ -18,8 +19,7 @@ use crate::compact_model_fallback::record_model_fallback; use crate::compact_model_fallback::should_retry_with_current_model; use crate::compact_pending_input::MAX_RETAINED_AGENT_MESSAGE_TOKENS; use crate::compact_pending_input::is_retained_pending_mail; -use crate::compact_remote_history::HistoryItemGroup; -use crate::compact_remote_history::history_item_groups; +use crate::compact_remote_history::newest_history_item_groups; use crate::context::world_state::WorldState; use crate::context_manager::estimate_item_token_count; use crate::context_manager::estimate_item_token_count_with_audio; @@ -510,20 +510,18 @@ fn build_v2_compacted_history( &prompt_input, ); let pending_sequence = crate::compact_pending_input::pending_mail_sequence(&prompt_input); - let retained = v2_history_item_groups(prompt_input) - .filter(|group| { - is_retained_for_remote_compaction_v2(&group.source, features) - || pending_sequence - .is_some_and(|first| is_retained_pending_mail(&group.source, first)) - }) - .flat_map(HistoryItemGroup::into_items) - .collect::>(); - let retained = truncate_retained_messages( - retained, + let retained = select_retained_items( + &prompt_input, + |item| { + is_retained_for_remote_compaction_v2(item, features) + || pending_sequence.is_some_and(|first| is_retained_pending_mail(item, first)) + }, RETAINED_MESSAGE_TOKEN_BUDGET, image_budget, audio_durations, - ); + ) + .into_values() + .collect::>(); let retained_image_count = retained .iter() .map(|envelope| retained_input_image_count(&envelope.item)) @@ -540,6 +538,20 @@ fn build_v2_compacted_history( (retained, retained_image_count) } +pub(crate) fn retained_client_items( + items: &[ResponseItemEnvelope], + audio_durations: &FileAudioDurations, + should_retain: impl Fn(&ResponseItemEnvelope) -> bool, +) -> BTreeMap { + select_retained_items( + items, + should_retain, + RETAINED_MESSAGE_TOKEN_BUDGET, + RetainedImageBudget::Disabled, + audio_durations, + ) +} + pub(crate) fn is_client_authored_developer_message(item: &ResponseItemEnvelope) -> bool { item.metadata .as_ref() @@ -547,26 +559,20 @@ pub(crate) fn is_client_authored_developer_message(item: &ResponseItemEnvelope) && matches!(&item.item, ResponseItem::Message { role, .. } if role == "developer") } -fn v2_history_item_groups( - items: Vec, -) -> impl Iterator> { - history_item_groups(items).flat_map(|mut group| { - let client_message = group - .attached_notice - .take_if(|item| is_client_authored_developer_message(item)) - .map(|source| HistoryItemGroup { - source, - attached_notice: None, - }); - std::iter::once(group).chain(client_message) - }) +pub(crate) fn should_retain_client_item(item: &ResponseItemEnvelope, features: &Features) -> bool { + if let Some(retain) = item.metadata.as_ref().and_then(|m| m.retain_across_windows) { + return retain; + } + (features.enabled(Feature::RetainClientDeveloperMessages) + && is_client_authored_developer_message(item)) + || (features.enabled(Feature::RetainClientToolOutputs) && is_retained_tool_output(item)) } fn is_retained_for_remote_compaction_v2( envelope: &ResponseItemEnvelope, features: &Features, ) -> bool { - if features.enabled(Feature::RetainClientToolOutputs) && is_retained_tool_output(envelope) { + if should_retain_client_item(envelope, features) { return true; } let item = &envelope.item; @@ -605,10 +611,6 @@ fn is_retained_for_remote_compaction_v2( crate::event_mapping::parse_turn_item(item), Some(TurnItem::UserMessage(_) | TurnItem::HookPrompt(_)) ), - "developer" => { - features.enabled(Feature::RetainClientDeveloperMessages) - && is_client_authored_developer_message(envelope) - } _ => false, } } @@ -651,19 +653,32 @@ fn truncate_retained_messages( image_budget: RetainedImageBudget, audio_durations: &FileAudioDurations, ) -> Vec { + select_retained_items(&items, |_| true, max_tokens, image_budget, audio_durations) + .into_values() + .collect() +} + +/// Select newest first, then return the items at their original positions. +fn select_retained_items( + items: &[ResponseItemEnvelope], + should_retain: impl Fn(&ResponseItemEnvelope) -> bool, + max_tokens: usize, + image_budget: RetainedImageBudget, + audio_durations: &FileAudioDurations, +) -> BTreeMap { let mut remaining = max_tokens; - let mut truncated_reversed = Vec::with_capacity(items.len()); - for group in v2_history_item_groups(items) - .collect::>() - .into_iter() - .rev() - { + let mut selected = BTreeMap::new(); + for group in newest_history_item_groups(items) { if remaining == 0 { + break; + } + if !should_retain(group.source) { continue; } - let client_developer = is_client_authored_developer_message(&group.source); - let image_budget = if client_developer { + let charge_full_item = + group.source.is_always_retained() || is_client_authored_developer_message(group.source); + let image_budget = if charge_full_item { RetainedImageBudget::Disabled } else { image_budget @@ -677,7 +692,7 @@ fn truncate_retained_messages( // Charged media is truncated newest-first as a message boundary. Each // modality still defines its own grouping and token cost in `media`. let requires_media_boundary_truncation = has_budgeted_image || has_file_audio; - let notice_tokens = group.attached_notice.as_ref().map_or(0, |notice| { + let notice_tokens = group.attached_notice.map_or(0, |notice| { retained_message_token_count( ¬ice.item, RetainedImageBudget::Disabled, @@ -685,11 +700,10 @@ fn truncate_retained_messages( ) .max(1) }); - // Client-authored developer messages already charge non-text content via - // the serialized estimate. Preserve their text-only boundary correction. + // Explicitly retained items and client developer messages also pay for their media. let content_tokens = retained_message_token_count(&group.source.item, image_budget, audio_durations); - let source_tokens = if client_developer { + let source_tokens = if charge_full_item { usize::try_from(estimate_item_token_count_with_audio( &group.source.item, audio_durations, @@ -698,19 +712,23 @@ fn truncate_retained_messages( } else { content_tokens.max(1) }; - let token_count = source_tokens.saturating_add(notice_tokens); - if token_count <= remaining { - if let Some(notice) = group.attached_notice { - truncated_reversed.push(notice); - } - truncated_reversed.push(group.source); + let token_count = if group.tool_call.is_some() { + usize::try_from(group.estimated_token_count(audio_durations)).unwrap_or(usize::MAX) + } else { + source_tokens.saturating_add(notice_tokens) + }; + let source = if token_count <= remaining { remaining = remaining.saturating_sub(token_count); - } else if is_retained_tool_output(&group.source) { - // Keep outputs whole and stop before older items to preserve newest-first ordering. + group.source.clone() + } else if is_retained_tool_output(group.source) + || (group.source.is_always_retained() + && !matches!(group.source.item, ResponseItem::Message { .. })) + { + // Structured items such as tool results must stay whole. break; } else if remaining > notice_tokens { let available_tokens = remaining - notice_tokens; - let content_budget = if client_developer { + let content_budget = if charge_full_item { available_tokens.saturating_sub(source_tokens.saturating_sub(content_tokens)) } else { available_tokens @@ -722,18 +740,18 @@ fn truncate_retained_messages( } let truncated_item = if requires_media_boundary_truncation { media::truncate_message_to_token_budget( - group.source, + group.source.clone(), content_budget, image_budget, audio_durations, ) } else { - truncate_message_text_to_token_budget(group.source, content_budget) + truncate_message_text_to_token_budget(group.source.clone(), content_budget) }; let Some(mut truncated_item) = truncated_item else { continue; }; - if client_developer { + if charge_full_item { let item_tokens = usize::try_from(estimate_item_token_count_with_audio( &truncated_item.item, audio_durations, @@ -768,17 +786,16 @@ fn truncate_retained_messages( truncated_item = adjusted; } } - if let Some(notice) = group.attached_notice { - truncated_reversed.push(notice); - } - truncated_reversed.push(truncated_item); remaining = 0; + truncated_item } else if requires_media_boundary_truncation { - remaining = 0; - } + break; + } else { + continue; + }; + group.keep(&mut selected, source); } - truncated_reversed.reverse(); - truncated_reversed + selected } fn retained_message_token_count( diff --git a/codex-rs/core/src/compact_retained_tool_outputs.rs b/codex-rs/core/src/compact_retained_tool_outputs.rs index 0ea3b68f10..c86deeaf50 100644 --- a/codex-rs/core/src/compact_retained_tool_outputs.rs +++ b/codex-rs/core/src/compact_retained_tool_outputs.rs @@ -1,5 +1,4 @@ -//! Retains explicitly marked, named function outputs that have no matching call. -//! Outputs share the normal history token budget and are retained whole or dropped. +//! Identifies standalone retained tool outputs in model history. use codex_history::ResponseItemEnvelope; use codex_protocol::models::ResponseItem; @@ -8,7 +7,7 @@ pub(crate) fn is_retained_tool_output(envelope: &ResponseItemEnvelope) -> bool { envelope .metadata .as_ref() - .is_some_and(|metadata| metadata.client_authored) + .is_some_and(|m| m.retain_across_windows.unwrap_or(m.client_authored)) && matches!( envelope.item, ResponseItem::FunctionCallOutput { diff --git a/codex-rs/core/src/compact_tests.rs b/codex-rs/core/src/compact_tests.rs index a57d2f0240..32e8026225 100644 --- a/codex-rs/core/src/compact_tests.rs +++ b/codex-rs/core/src/compact_tests.rs @@ -272,6 +272,15 @@ fn compacted_user_message<'a>(text: &str, original: &'a ResponseItem) -> Compact } } +fn compact_items(items: &[ResponseItemEnvelope], summary: &str) -> Vec { + build_compacted_history_with_limit( + items, + summary, + COMPACT_USER_MESSAGE_MAX_TOKENS, + &FileAudioDurations::default(), + ) +} + #[tokio::test] async fn extension_window_prefix_precedes_retained_turns_and_is_not_repeated() { use codex_extension_api::PreviousWorldStateSection; @@ -429,7 +438,9 @@ fn collect_annotated_user_messages_extracts_user_text_only( ResponseItemEnvelope::new(ResponseItem::Other), ]; - let collected = collect_annotated_user_messages(&items); + let collected: Vec<_> = annotated_user_messages(&items) + .map(|(_, message)| message) + .collect(); if !preserve_content { item = user_message(expected_text); @@ -458,7 +469,7 @@ fn collect_annotated_user_messages_extracts_user_text_only( }] ); assert_eq!( - build_compacted_history(Vec::new(), &collected, "summary"), + compact_items(&items, "summary"), vec![ expected, ResponseItemEnvelope::new(ContextualUserFragment::into(CompactionSummary::new( @@ -576,10 +587,10 @@ fn build_token_limited_compacted_history_truncates_overlong_user_messages( .set_id(Some(ResponseItemId::with_suffix("msg", "long-user"))); original.metadata = Some(CodexHarnessMetadata::default()); let history = super::build_compacted_history_with_limit( - Vec::new(), - &collect_annotated_user_messages(std::slice::from_ref(&original)), + std::slice::from_ref(&original), "SUMMARY", max_tokens, + &FileAudioDurations::default(), ); assert_eq!(history.len(), 2); @@ -653,12 +664,10 @@ fn build_token_limited_compacted_history_truncates_overlong_user_messages( #[test] fn build_token_limited_compacted_history_appends_summary_message() { - let initial_context: Vec = Vec::new(); - let original = user_message("first user message"); - let user_messages = collect_user_messages(std::slice::from_ref(&original)); + let items = annotated(vec![user_message("first user message")]); let summary_text = "summary text"; - let history = build_compacted_history(initial_context, &user_messages, summary_text); + let history = compact_items(&items, summary_text); assert!( !history.is_empty(), "expected compacted history to include summary" @@ -704,11 +713,7 @@ fn build_compacted_history_preserves_user_message_passthrough_metadata(attribute item: original, metadata: Some(CodexHarnessMetadata::default()), }; - let history = build_compacted_history( - Vec::new(), - &collect_annotated_user_messages(std::slice::from_ref(&original)), - "summary text", - ); + let history = compact_items(std::slice::from_ref(&original), "summary text"); assert_eq!( history, diff --git a/codex-rs/core/src/session/inject.rs b/codex-rs/core/src/session/inject.rs index 7b478371fb..d7d9bc6fe5 100644 --- a/codex-rs/core/src/session/inject.rs +++ b/codex-rs/core/src/session/inject.rs @@ -7,6 +7,8 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelInfo; +use std::sync::Arc; +use tokio_util::sync::CancellationToken; /// Uploaded files need the admission checks on typed turn input before entering history. pub(crate) fn validate_injected_response_item(item: &ResponseItem) -> CodexResult<()> { @@ -19,6 +21,45 @@ pub(crate) fn validate_injected_response_item(item: &ResponseItem) -> CodexResul } impl Session { + /// History injection can precede the first turn. Capture full initial context + /// only when the thread has no reference context, including after compaction. + pub(crate) async fn history_injection_context( + self: &Arc, + ) -> CodexResult> { + if self.reference_context_item().await.is_some() { + let turn = self.new_inject_items_context().await; + // Compaction can clear the baseline while we build this context. + // Rebuilding initial context requires a full skills snapshot. + if self.reference_context_item().await.is_some() { + return Ok(turn); + } + } + let turn = self.new_default_turn().await; + if self.reference_context_item().await.is_none() { + let step = self + .capture_step_context(Arc::clone(&turn), &CancellationToken::new()) + .await?; + self.record_context_updates_and_set_reference_context_item(step.as_ref()) + .await?; + } + Ok(turn) + } + + pub(crate) async fn record_inherited_items( + self: &Arc, + items: Vec, + ) -> CodexResult<()> { + if items.is_empty() { + return Ok(()); + } + let turn = self.history_injection_context().await?; + // The parent already prepared the images and their dimension notices. + self.record_prepared_conversation_items(&turn, turn.model_info(), items, Vec::new()) + .await; + self.flush_rollout().await?; + Ok(()) + } + /// Returns the input if there is no active turn to inject into. #[expect( clippy::await_holding_invalid_type, @@ -76,19 +117,19 @@ impl Session { Ok(()) } - /// Preserves trusted client provenance while items wait for an active turn. + /// Applies client developer-message retention before queueing or recording items. #[expect( clippy::await_holding_invalid_type, reason = "active turn checks and turn state updates must remain atomic" )] pub(crate) async fn inject_client_response_items( &self, - items: Vec, + items: Vec>, turn_context: &TurnContext, ) { let items = items .into_iter() - .map(ResponseItemEnvelope::new) + .map(Into::into) .map(|item| self.annotate_client_response_item(item)) .collect::>(); let mut active = self.active_turn.lock().await; @@ -120,7 +161,11 @@ impl Session { if self.enabled(Feature::RetainClientDeveloperMessages) && matches!(&envelope.item, ResponseItem::Message { role, .. } if role == "developer") { - envelope.metadata.get_or_insert_default().client_authored = true; + envelope + .metadata + .get_or_insert_default() + .retain_across_windows + .get_or_insert(true); } envelope } diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 57d0b39a1f..f5b3c0b595 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -225,8 +225,6 @@ use uuid::Uuid; use crate::client::ModelClient; use crate::codex_thread::CodexThreadSettingsOverrides; use crate::codex_thread::ThreadConfigSnapshot; -#[cfg(test)] -use crate::compact::collect_user_messages; use crate::config::Config; use crate::config::ConstraintResult; use crate::config::PermissionProfileSnapshot; @@ -689,7 +687,15 @@ impl Session { }) } - async fn spawn_internal(args: SessionSpawnArgs) -> CodexResult<(Arc, SessionIo)> { + // Construct the startup future outside the caller's poll frame. + #[inline(never)] + fn spawn_internal( + args: SessionSpawnArgs, + ) -> BoxFuture<'static, CodexResult<(Arc, SessionIo)>> { + Box::pin(Self::initialize(args)) + } + + async fn initialize(args: SessionSpawnArgs) -> CodexResult<(Arc, SessionIo)> { args.config .config_layer_stack .ensure_config_server_applied()?; @@ -5472,24 +5478,16 @@ impl Session { let turn_context = step_context.turn.as_ref(); let history = self.clone_history().await; let input_goal_ids = crate::context::UserGoalUpdate::message_ids(history.raw_items()); - let retained_client_items = - crate::compact_remote_v2::truncate_retained_messages_for_remote_compaction( - history - .annotated_items() - .iter() - .filter(|item| { - (self.enabled(Feature::RetainClientToolOutputs) - && crate::compact_retained_tool_outputs::is_retained_tool_output(item)) - || (self.enabled(Feature::RetainClientDeveloperMessages) - && crate::compact_remote_v2::is_client_authored_developer_message( - item, - )) - }) - .cloned() - .collect(), - crate::compact_remote_v2::RETAINED_MESSAGE_TOKEN_BUDGET, - &history.file_audio_durations, - ); + let retained_client_items = crate::compact_remote_v2::retained_client_items( + history.annotated_items(), + &history.file_audio_durations, + |item| { + crate::compact_remote_v2::should_retain_client_item( + item, + &turn_context.config.features, + ) + }, + ); // Resolve the roster before allocating a new context window. let rendered = world_state.render_full_with_host_roster().await; let (window_number, window_ids) = self.state.lock().await.start_new_context_window(); @@ -5499,7 +5497,7 @@ impl Session { crate::context_manager::updates::merge_world_state_updates(context_updates) .into_iter() .map(ResponseItemEnvelope::new) - .chain(retained_client_items) + .chain(retained_client_items.into_values()) .collect(); let turn_context_item = step_context.to_turn_context_item(); self.replace_compacted_history( diff --git a/codex-rs/core/src/session/rollout_reconstruction.rs b/codex-rs/core/src/session/rollout_reconstruction.rs index b5e37406b3..dc5e73aa93 100644 --- a/codex-rs/core/src/session/rollout_reconstruction.rs +++ b/codex-rs/core/src/session/rollout_reconstruction.rs @@ -561,13 +561,8 @@ impl ContextManager { // prompt shape. // TODO(ccunningham): if we drop support for None replacement_history compaction items, // we can get rid of this second loop entirely and just build `history` directly in the first loop. - let user_messages = - compact::collect_annotated_user_messages(history.annotated_items()); - let rebuilt = compact::build_compacted_history( - Vec::new(), - &user_messages, - &compacted.message, - ); + let rebuilt = + compact::build_compacted_history(&history, &compacted.message); let retained_context = history.retained_context().clone(); history.replace_annotated(rebuilt); history.restore_retained_context(Some(&retained_context)); diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 98fa46b3ae..a146a524de 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1017,6 +1017,10 @@ impl Session { SessionId::from(thread_id) } }); + let inherited_items = agent_control + .runtime() + .retained_items_for_fresh_child(&initial_history, &session_source) + .await; let has_provided_agent_control = matches!(&agent_control, AgentControlInit::Provided { .. }); let (agent_control, local_agent_runtime): (Arc, _) = match agent_control { @@ -2129,6 +2133,7 @@ impl Session { // record_initial_history can emit events. We record only after the SessionConfiguredEvent is emitted. Box::pin(sess.record_initial_history(initial_history)).await; + Box::pin(sess.record_inherited_items(inherited_items)).await?; if restore_child_window { sess.state.lock().await.restore_auto_compact_window( /*window_number*/ 0, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 85295c864b..b2fcaf1733 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -13433,9 +13433,7 @@ async fn sample_rollout( rollout_items.push(RolloutItem::ResponseItem(assistant1.clone().into())); let summary1 = "summary one"; - let snapshot1 = raw_history_items(&live_history); - let user_messages1 = collect_user_messages(&snapshot1); - let rebuilt1 = compact::build_compacted_history(Vec::new(), &user_messages1, summary1); + let rebuilt1 = compact::build_compacted_history(&live_history, summary1); live_history.replace_annotated(rebuilt1); let (window_number, window_ids) = session.advance_auto_compact_window().await; rollout_items.push(RolloutItem::Compacted(Box::new(CompactedItem { @@ -13468,9 +13466,7 @@ async fn sample_rollout( rollout_items.push(RolloutItem::ResponseItem(assistant2.clone().into())); let summary2 = "summary two"; - let snapshot2 = raw_history_items(&live_history); - let user_messages2 = collect_user_messages(&snapshot2); - let rebuilt2 = compact::build_compacted_history(Vec::new(), &user_messages2, summary2); + let rebuilt2 = compact::build_compacted_history(&live_history, summary2); live_history.replace_annotated(rebuilt2); let (window_number, window_ids) = session.advance_auto_compact_window().await; rollout_items.push(RolloutItem::Compacted(Box::new(CompactedItem { diff --git a/codex-rs/core/src/session/turn_input.rs b/codex-rs/core/src/session/turn_input.rs index d8c41b873d..470d28d3dc 100644 --- a/codex-rs/core/src/session/turn_input.rs +++ b/codex-rs/core/src/session/turn_input.rs @@ -1127,7 +1127,7 @@ async fn pending_turn_input( SubmittedTurnInput::AnnotatedResponseItem(input) => input.annotations.retain, _ => false, }; - let mut input = match input { + match input { SubmittedTurnInput::UserInput { content, client_id } => TurnInput::UserInput { content, client_id, @@ -1143,7 +1143,7 @@ async fn pending_turn_input( ResponseItem::FunctionCallOutput { call_id: None, .. } ) => { - let metadata = if let Some(fragment) = sender_messages { + let mut metadata = if let Some(fragment) = sender_messages { let mut messages = SenderUserMessages { receiver_turn_id: turn_id.to_owned(), receiver_message_id: fragment.delivery.clone(), @@ -1158,6 +1158,12 @@ async fn pending_turn_input( } else { None }; + if retain + && session.enabled(Feature::RetainClientToolOutputs) + && matches!(item, ResponseItem::FunctionCallOutput { name: Some(_), .. }) + { + metadata.get_or_insert_default().retain_across_windows = Some(true); + } TurnInput::FunctionCallOutput(ResponseItemEnvelope { item, metadata }) } SubmittedTurnInput::ResponseItem(item) @@ -1167,13 +1173,5 @@ async fn pending_turn_input( SubmittedTurnInput::InterAgentCommunication(communication) => { TurnInput::InterAgentCommunication(communication.into()) } - }; - if retain - && let TurnInput::ResponseItem(envelope) | TurnInput::FunctionCallOutput(envelope) = - &mut input - && matches!(envelope.item, ResponseItem::FunctionCallOutput { .. }) - { - envelope.metadata.get_or_insert_default().client_authored = true; } - input } diff --git a/codex-rs/core/src/tools/executed_tool_calls.rs b/codex-rs/core/src/tools/executed_tool_calls.rs index d5bc89f5b1..52df442d30 100644 --- a/codex-rs/core/src/tools/executed_tool_calls.rs +++ b/codex-rs/core/src/tools/executed_tool_calls.rs @@ -766,7 +766,7 @@ fn recorded_call(call: &ToolCall) -> (ExecutedToolCall, usize) { (ExecutedToolCall::new(name, arguments), original_bytes) } -fn input_call_id(item: &ResponseItem) -> Option<&str> { +pub(crate) fn input_call_id(item: &ResponseItem) -> Option<&str> { match item { ResponseItem::FunctionCall { call_id, .. } | ResponseItem::CustomToolCall { call_id, .. } => Some(call_id), @@ -776,7 +776,7 @@ fn input_call_id(item: &ResponseItem) -> Option<&str> { } } -fn output_call_id(item: &ResponseItem) -> Option<&str> { +pub(crate) fn output_call_id(item: &ResponseItem) -> Option<&str> { match item { ResponseItem::FunctionCallOutput { call_id, .. } | ResponseItem::ToolSearchOutput { call_id, .. } => call_id.as_deref(), @@ -784,3 +784,23 @@ fn output_call_id(item: &ResponseItem) -> Option<&str> { _ => None, } } + +pub(crate) fn is_matching_tool_output(call: &ResponseItem, output: &ResponseItem) -> bool { + let Some(call_id) = input_call_id(call) else { + return false; + }; + output_call_id(output) == Some(call_id) + && matches!( + (call, output), + ( + ResponseItem::FunctionCall { .. } | ResponseItem::LocalShellCall { .. }, + ResponseItem::FunctionCallOutput { .. } + ) | ( + ResponseItem::CustomToolCall { .. }, + ResponseItem::CustomToolCallOutput { .. } + ) | ( + ResponseItem::ToolSearchCall { .. }, + ResponseItem::ToolSearchOutput { .. } + ) + ) +} diff --git a/codex-rs/core/src/tools/mod.rs b/codex-rs/core/src/tools/mod.rs index eebc88af8b..3bded5593d 100644 --- a/codex-rs/core/src/tools/mod.rs +++ b/codex-rs/core/src/tools/mod.rs @@ -41,6 +41,9 @@ use codex_utils_output_truncation::TruncationPolicy; use codex_utils_output_truncation::formatted_truncate_text; use codex_utils_output_truncation::truncate_text; pub(crate) use executed_tool_calls::ExecutedToolCalls; +pub(crate) use executed_tool_calls::input_call_id; +pub(crate) use executed_tool_calls::is_matching_tool_output; +pub(crate) use executed_tool_calls::output_call_id; pub use multi_agent_tool::MULTI_AGENT_V2_NAMESPACE_DESCRIPTION; pub use router::ToolRouter; diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 2f65cf709d..71ca48832d 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -439,11 +439,7 @@ async fn remote_compact_v2_retains_only_client_developer_messages_when_enabled( .context("remote compaction should persist a checkpoint")?; let retained_client_developers = replacement_history .iter() - .filter(|item| { - item.metadata - .as_ref() - .is_some_and(|metadata| metadata.client_authored) - }) + .filter(|item| item.is_always_retained()) .count(); assert_eq!(retained_client_developers, usize::from(enabled)); Ok(()) diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 668445aef5..dc27b7e0c3 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -877,11 +877,11 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R else { return None; }; - matches!(envelope.item, ResponseItem::Message { role, .. } if role == "developer") - .then(|| envelope.metadata.map(|metadata| metadata.client_authored)) + matches!(&envelope.item, ResponseItem::Message { role, .. } if role == "developer") + .then(|| envelope.is_always_retained()) }) .collect::>(); - assert_eq!(persisted_developer_metadata, vec![Some(false), Some(true)]); + assert_eq!(persisted_developer_metadata, vec![false, true]); Ok(()) } diff --git a/codex-rs/core/tests/suite/scenarios_compaction_tests.rs b/codex-rs/core/tests/suite/scenarios_compaction_tests.rs index ddd1ddaa1f..ec46adecd4 100644 --- a/codex-rs/core/tests/suite/scenarios_compaction_tests.rs +++ b/codex-rs/core/tests/suite/scenarios_compaction_tests.rs @@ -1,6 +1,7 @@ //! Snapshots pending mail across remote compaction, restart, and normal inference. use super::*; +use codex_protocol::turn_input::InjectedResponseItem; use core_test_support::responses::strip_metadata_from_json; use pretty_assertions::assert_eq; @@ -52,8 +53,20 @@ async fn remote_slash_compact_preserves_context_across_windows() -> Result<()> { "author": "/root/worker", "recipient": "/root", "content": [{"type": "input_text", "text": "Message Type: FINAL_ANSWER\nThe worker found a release blocker."}], }); + let call: InjectedResponseItem = serde_json::from_value(json!({ + "item": {"type": "function_call", "call_id": "release-check", "name": "check_release", "arguments": "{}"}, + "retention": "always" + }))?; + let result: InjectedResponseItem = serde_json::from_value(json!({ + "item": {"type": "function_call_output", "call_id": "release-check", "output": "The release candidate passed."}, + "retention": "always" + }))?; test.codex - .inject_response_items(vec![serde_json::from_value(mail.clone())?]) + .inject_response_items_with_retention(vec![ + call, + result, + serde_json::from_value::(mail.clone())?.into(), + ]) .await?; for compaction in 0..2 { test.codex.submit(Op::Compact).await?; diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap index 2efbf44dad..58d7ed1741 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap @@ -104,8 +104,10 @@ Settings: -- request 2 (compaction; Remote /compact request) -- 08:message/assistant: The release plan has three steps. -09:agent_message -10:compaction_trigger +09:function_call/check_release:{} +10:function_call_output:The release candidate passed. +11:agent_message +12:compaction_trigger ## Window 2 (after request 2: input diverged at item 07) Settings: same as window 1 @@ -194,9 +196,11 @@ Settings: same as window 1 06:message/user: Plan the release. -07:compaction:encrypted=true; chars=31; hash=8255caf138edede8 -08:agent_message -09:compaction_trigger +07:function_call/check_release:{} +08:function_call_output:The release candidate passed. +09:compaction:encrypted=true; chars=31; hash=8255caf138edede8 +10:agent_message +11:compaction_trigger ## Window 3 (after request 3: input diverged at item 02) Settings: same as window 1 @@ -298,22 +302,24 @@ Settings: same as window 1 06:message/user: Plan the release. -07:compaction:encrypted=true; chars=22; hash=5180312a53382d23 -08:agent_message -09:message/developer: +07:function_call/check_release:{} +08:function_call_output:The release candidate passed. +09:compaction:encrypted=true; chars=22; hash=5180312a53382d23 +10:agent_message +11:message/developer: Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. -10:message/user: +12:message/user: [hash=46e2a5c52f0891de] -11:message/user: +13:message/user: Continue with the first step. -- request 5 (compaction; Compaction after mail was sampled) -- -12:message/assistant: +14:message/assistant: Start by validating the release candidate. -13:compaction_trigger +15:compaction_trigger diff --git a/codex-rs/core/tests/suite/turn_input_submission.rs b/codex-rs/core/tests/suite/turn_input_submission.rs index 1c505abfd7..59168fec8b 100644 --- a/codex-rs/core/tests/suite/turn_input_submission.rs +++ b/codex-rs/core/tests/suite/turn_input_submission.rs @@ -1807,13 +1807,7 @@ async fn tool_output_retention_is_submission_metadata() -> anyhow::Result<()> { else { return None; }; - Some(( - name.as_deref().unwrap(), - envelope - .metadata - .as_ref() - .is_some_and(|metadata| metadata.client_authored), - )) + Some((name.as_deref().unwrap(), envelope.is_always_retained())) }) .collect::>(); assert_eq!( diff --git a/codex-rs/deny.toml b/codex-rs/deny.toml index a3e89f3c18..7e89bd7aab 100644 --- a/codex-rs/deny.toml +++ b/codex-rs/deny.toml @@ -95,6 +95,10 @@ ignore = [ { id = "RUSTSEC-2026-0511", reason = "hickory-proto 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2" }, { id = "RUSTSEC-2026-0512", reason = "hickory-proto 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2" }, { id = "RUSTSEC-2026-0514", reason = "hickory-resolver 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2" }, + # Additional DNSSEC-only advisories reviewed 2026-11-13. + { id = "RUSTSEC-2026-0530", reason = "hickory-proto 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2" }, + { id = "RUSTSEC-2026-0531", reason = "hickory-proto 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2" }, + { id = "RUSTSEC-2026-0532", reason = "hickory-proto 0.25.2 via rama-dns/rama-tcp in codex-network-proxy; DNSSEC is disabled even with all workspace features; reassess before enabling DNSSEC; remove after the Rust/Rama upgrade uses Hickory >=0.26.2" }, { id = "RUSTSEC-2026-0194", reason = "quick-xml v0.39.4 remains via plist/syntect and wayland-scanner; plist does not exercise the affected APIs and wayland-scanner parses trusted build-time XML; remove when rust-plist#191 and wayland-rs#938 are released" }, { id = "RUSTSEC-2026-0195", reason = "quick-xml v0.39.4 remains via plist/syntect and wayland-scanner; plist does not exercise the affected APIs and wayland-scanner parses trusted build-time XML; remove when rust-plist#191 and wayland-rs#938 are released" }, { id = "RUSTSEC-2026-0370", reason = "zvariant 4.2.0 is unmaintained; pulled in via zbus 4.4.0 by Linux TUI accessibility integration and keyring/secret-service; remove when those paths migrate off the retired standalone crates" }, diff --git a/codex-rs/history/src/lib.rs b/codex-rs/history/src/lib.rs index eacc5a49fe..cbf3934e42 100644 --- a/codex-rs/history/src/lib.rs +++ b/codex-rs/history/src/lib.rs @@ -46,6 +46,8 @@ use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::WorldStateItem; use codex_protocol::realtime::RealtimeItem; use codex_protocol::security_risk::SecurityRiskScore; +use codex_protocol::turn_input::InjectedResponseItem; +use codex_protocol::turn_input::ItemRetention; use schemars::JsonSchema; use serde::Deserialize; use serde::Deserializer; @@ -104,9 +106,17 @@ pub struct CodexHarnessMetadata { pub additional_context_tool_output: bool, /// Whether a developer message was supplied by a client or a client requested tool-output retention. + /// + /// Use this field only to interpret older rollouts. + /// Store new retention decisions in `retain_across_windows`. #[serde(default)] pub client_authored: bool, + /// Per-item override of the thread's context-window retention policy. + /// Never sent to the model. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub retain_across_windows: Option, + /// The originating history budget, including any tool-specific allowance. /// Measured in tokens and reused when replaying persisted history. #[serde( @@ -211,6 +221,13 @@ impl CodexHarnessMetadata { } impl ResponseItemEnvelope { + /// Whether the client requested that this item survive context-window changes and forks. + pub fn is_always_retained(&self) -> bool { + self.metadata + .as_ref() + .is_some_and(|metadata| metadata.retain_across_windows == Some(true)) + } + /// Wraps a raw Responses API item for persisted history. pub fn new(item: ResponseItem) -> Self { Self { @@ -231,6 +248,19 @@ impl From for ResponseItemEnvelope { } } +impl From for ResponseItemEnvelope { + fn from(injected: InjectedResponseItem) -> Self { + let mut envelope = Self::new(injected.item); + if injected.retention != ItemRetention::Unspecified { + envelope + .metadata + .get_or_insert_default() + .retain_across_windows = Some(injected.retention == ItemRetention::Always); + } + envelope + } +} + impl Deref for ResponseItemEnvelope { type Target = ResponseItem; diff --git a/codex-rs/protocol/src/turn_input.rs b/codex-rs/protocol/src/turn_input.rs index 93fa948cdb..c0ee688571 100644 --- a/codex-rs/protocol/src/turn_input.rs +++ b/codex-rs/protocol/src/turn_input.rs @@ -10,6 +10,7 @@ use crate::protocol::W3cTraceContext; use crate::user_input::UserInput; use schemars::JsonSchema; use serde::Deserialize; +use serde::Deserializer; use serde::Serialize; use serde_json::Value; use std::collections::BTreeMap; @@ -52,6 +53,55 @@ pub struct ResponseItemAnnotations { pub retain: bool, } +/// A Responses item for history injection. Wrapped items must specify a policy. +#[derive(Clone, Debug)] +pub struct InjectedResponseItem { + pub item: ResponseItem, + pub retention: ItemRetention, +} + +impl<'de> Deserialize<'de> for InjectedResponseItem { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct WrappedItem { + item: ResponseItem, + retention: ItemRetention, + } + + let value = Value::deserialize(deserializer)?; + if value.get("type").is_some() { + return ResponseItem::deserialize(value) + .map(Self::from) + .map_err(serde::de::Error::custom); + } + + let WrappedItem { item, retention } = + WrappedItem::deserialize(value).map_err(serde::de::Error::custom)?; + Ok(Self { item, retention }) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ItemRetention { + /// Raw items use the thread's configured retention rules. + #[serde(skip)] + Unspecified, + /// Apply ordinary history rules even when the thread retains client developer messages. + Default, + /// Carry the item into later context windows and newly spawned children. + Always, +} + +impl From for InjectedResponseItem { + fn from(item: ResponseItem) -> Self { + Self { + item, + retention: ItemRetention::Unspecified, + } + } +} + /// One turn input and the context that follows it through submission. /// /// Callers choose start-or-steer, idle-start, or steer-only behavior through