From c33002259e28db9e87807cedebfb25299d95a2c6 Mon Sep 17 00:00:00 2001 From: Vivian Fang Date: Fri, 13 Nov 2026 14:36:56 +0000 Subject: [PATCH] Add per-item retention policies for injected history (#61344) ## What changed Allow `thread/inject_items` to accept wrapped items with `retention: "default"` or `retention: "always"`, while preserving support for raw response items. Persist explicit policies separately from legacy client-authored metadata. Carry explicitly retained items through local and remote compaction, context-window changes, and child-agent creation, subject to retention budgets. Preserve history order and keep retained tool exchanges together; require matching calls and outputs to be adjacent in the injection request and both marked `always`. Update Quinn dependencies and record exceptions for additional Hickory DNSSEC advisories where DNSSEC is disabled. ## Testing Update injection tests for explicit retention metadata and extend the compaction scenario to cover a retained tool exchange across repeated compaction and restart. GitOrigin-RevId: 1195a4ed62dc162a62e58bc2ad733262fbabea27 --- MODULE.bazel.lock | 6 +- codex-rs/.cargo/audit.toml | 4 + codex-rs/Cargo.lock | 20 +-- .../app-server-exports-stable.json.zst | Bin 170570 -> 170647 bytes .../src/request_processors/turn_processor.rs | 9 +- .../tests/suite/v2/thread_inject_items.rs | 30 ++-- codex-rs/core/src/agent/history.rs | 56 ++++++- codex-rs/core/src/codex_thread.rs | 62 +++++--- codex-rs/core/src/compact.rs | 64 ++++---- codex-rs/core/src/compact_remote_history.rs | 137 +++++++++-------- codex-rs/core/src/compact_remote_v2.rs | 143 ++++++++++-------- .../core/src/compact_retained_tool_outputs.rs | 5 +- codex-rs/core/src/compact_tests.rs | 31 ++-- codex-rs/core/src/session/inject.rs | 53 ++++++- codex-rs/core/src/session/mod.rs | 42 +++-- .../src/session/rollout_reconstruction.rs | 9 +- codex-rs/core/src/session/session.rs | 5 + codex-rs/core/src/session/tests.rs | 8 +- codex-rs/core/src/session/turn_input.rs | 18 +-- .../core/src/tools/executed_tool_calls.rs | 24 ++- codex-rs/core/src/tools/mod.rs | 3 + codex-rs/core/tests/suite/compact_remote.rs | 6 +- codex-rs/core/tests/suite/image_rollout.rs | 6 +- .../tests/suite/scenarios_compaction_tests.rs | 15 +- ...ios__compaction__remote_slash_compact.snap | 30 ++-- .../core/tests/suite/turn_input_submission.rs | 8 +- codex-rs/deny.toml | 4 + codex-rs/history/src/lib.rs | 30 ++++ codex-rs/protocol/src/turn_input.rs | 50 ++++++ 29 files changed, 578 insertions(+), 300 deletions(-) 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 b08be3e4b067fe0df1d18eb0db40a777305cacfc..c4ed2837ec555d0709b7f80381efdd07848614d2 100644 GIT binary patch delta 24094 zcmV(cK>feUvzYT#xBsod5l#=2^u`{G9gT>Je zP_LsX2LeI>Kmb7iPg|pD)jQw!I+7K%CJ9R0T!e0K*-GU_H%ZFrPJOC!%LefNErM3> zz|s?3PL+3Jm0Bay+{Se95u(=O<%LiNBh~E`H|ceAtaZ?|iln_i?$fA|e|2c7e>Z+( z`WV+FOikT%D?4;>-v8OJ`SWLYSIvhQJFnVin&)r-{CbL45Lf}4+?LfT*7IKKy|bV2 z3#43dnh$r4QaFuo*Tl;ht8@-o>t}P>;5rm^X@cV zxd%CYZ{y~*=d-%P4M&!Ne^Ke0IPkYaXVTV948t%=~CCW8{O~GoC`snZ=Zx88_hVlqe=bX56W-OzEgeGoI&jkc@BiG zs*~C6g$nse?xdBIifD)ckK7~_6_wK0shTTRj{qO8Qc^85fAl(ETSYF#SjdFsd`vDA z7OVauSL0X~b?w{)JxcmT=~_;5QmVbwmx7)GG{A8l8K+_yLva>|5okI-{kNcnJ@RqN^PEMHm9ckT2mX9M3T<`8;$n467whxWElonTB)c>|lvxS1WK zetKt^Evv!cf0jF|6Wo!c98_>QtRo}Lyd$Zur3>d%m3ZBGy}7wWp$v)=RdZfk zwB#nv@#PfvA9!t#aC;Sv;_VSGWVJ#)+#}#yd(vDJf5!R$0r}uL*VEo3Xe?lArSB2YNp;pO_IrejZ?V~IKK~*zK#<@gQ2ehQnFS3V z9Jjfv3m<_zeY9bob5e96Y?~4vA?#)(wdZ^(w08z_W_*N;0iquCIqyNA<-y4yA7PCK zXb#Coe;~e?>|8zh2hckE2bfXGd<3Kc9dib2KEf?nSWM1GI7POa zw9E4me9hUCfIh+%R6|ZU^bxSWI}*M!Cxk3hb64Oyiz8tP5#22%AOtd3s!^}>#7oNU z82y`}sye_T@uSVB~g(M{OlTXbFQzi8J*eT$C& z%0D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�S00AO^!le)@TwXi6Jjeg= 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 delta 24016 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Π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 + 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