From 9c4253ffc1b954337bf2f494aadc55e9cd132a48 Mon Sep 17 00:00:00 2001 From: felixxia-oai Date: Fri, 4 Sep 2026 19:51:52 +0000 Subject: [PATCH] Retain user instructions in Guardian context (#42844) ## Why Guardian reviews need the original user instructions even when compaction or transcript selection removes them from the active conversation window. ## What changed - Capture complete user messages in host-owned retained context and include them as ordered authorization evidence alongside verified answers. - Omit oversized instructions atomically and emit an explicit incomplete-evidence notice instead of exposing a potentially misleading partial permission. - Mark user messages inherited by forked agents so they remain model-visible without becoming child-local authorization. ## Testing - Cover retained instructions across compaction, resume, rollback, and agent forks, including oversized evidence and legacy context behavior. GitOrigin-RevId: 97122509d818c90d2f87c6a1b29c99c05ad0316e --- .../app-server-exports-stable.json.zst | Bin 148071 -> 148121 bytes .../suite/v2/guardian_v2_history_tests.rs | 99 ++++--- codex-rs/core/src/agent/control/spawn.rs | 22 +- codex-rs/core/src/agent/control_tests.rs | 22 +- codex-rs/core/src/context_manager/history.rs | 11 +- codex-rs/core/src/guardian/prompt.rs | 8 + codex-rs/core/src/session/tests.rs | 5 +- .../handlers/request_user_input_tests.rs | 31 +- .../tests/suite/guardian_retained_context.rs | 272 +++++++++++++++++- codex-rs/core/tests/suite/hooks.rs | 3 +- .../src/capabilities/conversation_history.rs | 1 + .../src/async_scorer/extension_tests.rs | 156 +++++----- .../src/async_scorer/transcript.rs | 4 + codex-rs/guardian-context/src/composition.rs | 1 + codex-rs/guardian-context/src/lib.rs | 12 + .../src/retained_instructions.rs | 70 +++++ .../src/retained_instructions_tests.rs | 65 +++++ .../guardian-context/src/verified_answers.rs | 9 +- codex-rs/history/src/lib.rs | 4 + codex-rs/history/src/tests.rs | 8 +- 20 files changed, 663 insertions(+), 140 deletions(-) create mode 100644 codex-rs/guardian-context/src/retained_instructions.rs create mode 100644 codex-rs/guardian-context/src/retained_instructions_tests.rs 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 50afd0c1dcb52aef64d32180b9bdfc31a213ede1..63d861ae4424205e2d0a6105504c98e7e80a5652 100644 GIT binary patch delta 18663 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 KB>4t=R|`-K1G0Aj delta 18655 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 CMg, luna_hash: Option<&str>, tool_traffic: usize, - answer_size: AnswerSize, + evidence_size: EvidenceSize, ) -> Result<()> { skip_if_no_network!(Ok(())); @@ -94,9 +96,17 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac const SUMMARY: &str = "Repository inspection was summarized."; let compatible = parent_hash == Some("matching") && luna_hash == parent_hash; let requires_sync = matches!(context_path, ContextPath::ThreadOwned) && !compatible; - let answer = match answer_size { - AnswerSize::Normal => USER_INPUT_RESTRICTION.to_owned(), - AnswerSize::Oversized => "Never publish this repository publicly. ".repeat(200), + let oversized_instruction = matches!(evidence_size, EvidenceSize::OversizedInstruction); + let answer = match evidence_size { + EvidenceSize::Normal | EvidenceSize::OversizedInstruction => { + USER_INPUT_RESTRICTION.to_owned() + } + EvidenceSize::OversizedAnswer => "Never publish this repository publicly. ".repeat(200), + }; + let restriction = if oversized_instruction { + RESTRICTION.repeat(150) + } else { + RESTRICTION.to_owned() }; let expected_output = format!("\"echoed\":\"{EVIDENCE}\""); let parent_requests = Arc::new(Mutex::new(Vec::::new())); @@ -108,11 +118,11 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac "encrypted_content": "encrypted summary" }); let rejects_incomplete_score = matches!( - (context_path, answer_size), - (ContextPath::ThreadOwned, AnswerSize::Oversized) + (context_path, evidence_size), + (ContextPath::ThreadOwned, EvidenceSize::OversizedAnswer) ); let classifier = Arc::new(MockResponsesState { - luna_score: if rejects_incomplete_score || requires_sync { + luna_score: if rejects_incomplete_score || oversized_instruction || requires_sync { 0.0 } else { 1.0 @@ -270,7 +280,7 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac let thread_id = thread.id.clone(); for (index, prompt) in [ - RESTRICTION, + restriction.as_str(), "Recheck the repository.", "Inspect after resume.", "Inspect after partial rollback.", @@ -419,10 +429,14 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac .split_once(">>> TRANSCRIPT END") .expect("transcript end") .0; - assert!( - transcript.contains(prompt), - "current user input missing: {transcript}" - ); + if index == 0 && oversized_instruction { + assert!(content.contains("some retained user instructions are unavailable")); + } else { + assert!( + transcript.contains(prompt), + "current user input missing: {transcript}" + ); + } { let parent = parent_requests.lock().expect("request log lock"); @@ -527,12 +541,12 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac panic!("missing {consumer} answers at step {index}: {text}") }) .1; - match answer_size { - AnswerSize::Normal => { + match evidence_size { + EvidenceSize::Normal | EvidenceSize::OversizedInstruction => { assert!(answers.contains("assistant: Can I keep using the browser?")); assert!(answers.contains(&format!("user: {USER_INPUT_RESTRICTION}"))); } - AnswerSize::Oversized => match context_path { + EvidenceSize::OversizedAnswer => match context_path { ContextPath::ThreadOwned => { assert!( answers.contains("some verified user answers are unavailable") @@ -555,7 +569,7 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac } } classifier.allow_luna.notify_one(); - if rejects_incomplete_score || (index == 0 && requires_sync) { + if rejects_incomplete_score || oversized_instruction || (index == 0 && requires_sync) { // Wait for host publication, not merely a mock response. The next action must // see a fresh low score with unchanged authorization, rather than a stale score. let path = thread.path.as_ref().expect("legacy rollout path"); @@ -578,7 +592,14 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac }) .await?; } - if rejects_incomplete_score { + if rejects_incomplete_score || oversized_instruction { + let notice = match evidence_size { + EvidenceSize::OversizedInstruction => { + "some retained user instructions are unavailable" + } + EvidenceSize::OversizedAnswer => "some verified user answers are unavailable", + EvidenceSize::Normal => unreachable!("incomplete evidence fixture"), + }; let completed = app_server .start_turn_and_wait_for_completion(TurnStartParams { thread_id: thread_id.clone(), @@ -588,21 +609,19 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac .await?; assert_eq!(completed.turn.status, TurnStatus::Completed); let fresh_sample = wait_for_luna_request(&classifier, /*index*/ 2).await?; - assert!( - fresh_sample - .to_string() - .contains("some verified user answers are unavailable") - ); + assert!(fresh_sample.to_string().contains(notice)); let reviews = review_requests.lock().expect("request log lock"); assert_eq!( reviews.len(), - 2, - "an incomplete fresh score must still require sync review" + 1 + usize::from(rejects_incomplete_score), + "only incomplete verified answers require another sync review" ); assert!( - reviews[1] + reviews + .last() + .expect("initial sync review") .to_string() - .contains("some verified user answers are unavailable") + .contains(notice) ); classifier.allow_luna.notify_one(); break; diff --git a/codex-rs/core/src/agent/control/spawn.rs b/codex-rs/core/src/agent/control/spawn.rs index 998c7e1cd4..34f281df9e 100644 --- a/codex-rs/core/src/agent/control/spawn.rs +++ b/codex-rs/core/src/agent/control/spawn.rs @@ -16,6 +16,7 @@ use codex_context_fragments::set_annotated_content; use codex_context_fragments::to_annotated_content; use codex_extension_api::ExtensionDataInit; use codex_features::Feature; +use codex_history::ResponseItemEnvelope; use codex_protocol::intersect_effective_permission_profiles; use codex_protocol::protocol::EnvironmentConfigState; use codex_utils_path_uri::PathUri; @@ -932,7 +933,19 @@ impl AgentControl { // Scrub inherited hints and replace only the parent's developer-instruction fragment. // Compaction stores response items separately, so sanitize both top-level messages and // compacted replacement histories with the same policy. - let retain_forked_item = |response_item: &mut ResponseItem, replaced: &mut bool| { + let retain_forked_item = |envelope: &mut ResponseItemEnvelope, replaced: &mut bool| { + if config.features.enabled(Feature::GuardianThreadContext) + && multi_agent_version == MultiAgentVersion::V2 + && matches!(&envelope.item, ResponseItem::Message { role, .. } if role == "user") + { + // Persist the scope of every inherited user message, including the suffix + // after a checkpoint. Resume must not recapture it as local authorization. + envelope + .metadata + .get_or_insert_default() + .inherited_user_message = true; + } + let response_item = &mut envelope.item; if matches!(response_item, ResponseItem::AgentMessage { .. }) { return false; } @@ -1016,7 +1029,12 @@ impl AgentControl { // Parent-local review evidence must not become the child's authorization. // Root user authorization is collected separately by the host. compacted.guardian_history = None; - compacted.retained_context = None; + // Only V2 fetches root authorization live. Its local scope starts known-empty; + // V1 must remain incomplete when inherited authorization has been stripped. + compacted.retained_context = + (config.features.enabled(Feature::GuardianThreadContext) + && multi_agent_version == MultiAgentVersion::V2) + .then(codex_history::RetainedContext::default); if let Some(replacement_history) = compacted.replacement_history.as_mut() { // Matches before this checkpoint cannot survive its replacement history. replaced_parent_developer_instructions = false; diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index c7c605c256..61ebf719ba 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -1385,9 +1385,17 @@ async fn spawn_agent_without_fork_from_paginated_parent_stays_fresh_and_paginate .expect("parent shutdown should submit"); } +#[test_case::test_case(true; "thread context enabled")] +#[test_case::test_case(false; "thread context disabled")] #[tokio::test] -async fn spawn_agent_fork_drops_inherited_token_usage_state() { - let harness = AgentControlHarness::new().await; +async fn spawn_agent_fork_drops_inherited_token_usage_state(thread_context_enabled: bool) { + let mut harness = AgentControlHarness::new().await; + let _ = harness.config.features.disable(Feature::MultiAgentV2); + harness + .config + .features + .set_enabled(Feature::GuardianThreadContext, thread_context_enabled) + .expect("test context mode"); let (parent_thread_id, parent_thread) = harness.start_paginated_thread().await; let parent_usage = TokenUsage { total_tokens: 120, @@ -1445,6 +1453,15 @@ async fn spawn_agent_fork_drops_inherited_token_usage_state() { total_tokens: 80, ..TokenUsage::default() }; + assert!( + !child_thread + .session + .clone_history() + .await + .retained_context() + .user_messages_complete(), + "V1 forks lack complete retained authorization in both context modes" + ); let turn_context = child_thread.session.new_default_turn().await; child_thread .session @@ -2121,7 +2138,6 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history( ); let mut inherited_context = codex_history::RetainedContext::default(); if thread_context_enabled { - inherited_context.restore(/*checkpoint*/ None); inherited_context.reserve_order(); } else { inherited_context.mark_user_messages_incomplete(); diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index ab8cf48895..b2f6fd9a0c 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -87,6 +87,7 @@ struct SharedConversationHistory { items: Arc>, review_history: Option, retained_context: Arc, + expose_retained_context: bool, history_version: u64, user_message_revision: u64, } @@ -112,7 +113,8 @@ impl ConversationHistorySnapshot for SharedConversationHistory { } fn retained_context(&self) -> Option<&RetainedContext> { - Some(&self.retained_context) + self.expose_retained_context + .then_some(&self.retained_context) } fn review_items(&self) -> Box + Send + '_> { @@ -174,6 +176,7 @@ impl ContextManager { items: Arc::clone(&self.items), review_history: self.review_history.clone(), retained_context: Arc::clone(&self.retained_context), + expose_retained_context: self.retain_user_messages, history_version: self.history_version, user_message_revision: self.user_message_revision, }) @@ -329,7 +332,9 @@ impl ContextManager { } Arc::make_mut(&mut self.items).push(processed); if crate::context::is_user_authorization_message(item) { - if self.retain_user_messages + if !self.retain_user_messages { + Arc::make_mut(&mut self.retained_context).mark_user_messages_incomplete(); + } else if !metadata.is_some_and(|metadata| metadata.inherited_user_message) && let ResponseItem::Message { content, internal_chat_message_metadata_passthrough, @@ -365,8 +370,6 @@ impl ContextManager { }, metadata.and_then(|metadata| metadata.user_input_order), ); - } else { - Arc::make_mut(&mut self.retained_context).mark_user_messages_incomplete(); } self.user_message_revision = self.user_message_revision.saturating_add(1); } diff --git a/codex-rs/core/src/guardian/prompt.rs b/codex-rs/core/src/guardian/prompt.rs index 281d4dba16..1e408783a9 100644 --- a/codex-rs/core/src/guardian/prompt.rs +++ b/codex-rs/core/src/guardian/prompt.rs @@ -496,6 +496,10 @@ pub(super) fn collect_guardian_context( struct GuardianReviewHistory<'a>(&'a dyn ConversationHistorySnapshot); impl SectionHistory for GuardianReviewHistory<'_> { + fn retained_context(&self) -> Option<&codex_history::RetainedContext> { + self.0.retained_context() + } + fn items(&self) -> Box + Send + '_> { self.0.review_items() } @@ -504,6 +508,10 @@ impl SectionHistory for GuardianReviewHistory<'_> { struct FilteredGuardianHistory<'a>(&'a dyn SectionHistory); impl SectionHistory for FilteredGuardianHistory<'_> { + fn retained_context(&self) -> Option<&codex_history::RetainedContext> { + self.0.retained_context() + } + fn items(&self) -> Box + Send + '_> { Box::new(self.0.items().filter(|item| { !matches!( diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index f302a92a07..0847c59ce2 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -8783,7 +8783,10 @@ where session_configuration.session_source.clone(), ); - let state = SessionState::new(session_configuration.clone()); + let mut state = SessionState::new(session_configuration.clone()); + if config.features.enabled(Feature::GuardianThreadContext) { + state.history.enable_user_message_retention(); + } let (environment_manager, resolved_turn_environments) = resolved_environments_for_configuration(&session_configuration, &default_environments) .await; diff --git a/codex-rs/core/src/tools/handlers/request_user_input_tests.rs b/codex-rs/core/src/tools/handlers/request_user_input_tests.rs index 56f430d6a3..ddf261983f 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input_tests.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input_tests.rs @@ -196,16 +196,27 @@ async fn request_user_input_sets_non_blocking_outside_plan_mode( .expect("request_user_input handler should succeed"); assert!(output.success_for_logging()); let history = session.conversation_history_snapshot().await; - let RenderedVerifiedAnswers { - fragments, - complete, - } = codex_guardian_context::render_verified_answers( - history.retained_context().expect("host context snapshot"), - ); if thread_context_enabled { + let RenderedVerifiedAnswers { + fragments, + complete, + } = codex_guardian_context::render_verified_answers( + history.retained_context().expect("host context snapshot"), + ); + let expected_fragments = expected + .fragments + .iter() + .map(|fragment| { + if expected.complete { + format!("Retained source order: 0\n{fragment}") + } else { + fragment.clone() + } + }) + .collect::>(); assert_eq!( (&fragments, complete), - (&expected.fragments, expected.complete) + (&expected_fragments, expected.complete) ); } else { assert_eq!( @@ -239,7 +250,11 @@ async fn request_user_input_sets_non_blocking_outside_plan_mode( } _ => assert_eq!( legacy_answer, - fragments.first().cloned().filter(|_| complete) + expected + .fragments + .first() + .cloned() + .filter(|_| expected.complete) ), } } diff --git a/codex-rs/core/tests/suite/guardian_retained_context.rs b/codex-rs/core/tests/suite/guardian_retained_context.rs index 3c0c7b9442..75c4475fba 100644 --- a/codex-rs/core/tests/suite/guardian_retained_context.rs +++ b/codex-rs/core/tests/suite/guardian_retained_context.rs @@ -4,12 +4,18 @@ use std::collections::HashMap; use std::sync::Arc; +use std::time::Duration; use anyhow::Context; use anyhow::Result; use codex_core::CodexThread; use codex_core::TurnInputRequest; +use codex_core::config::Config; use codex_core::config::ThreadStoreConfig; +use codex_extension_api::ExtensionRegistryBuilder; +use codex_extension_api::ToolLifecycleContributor; +use codex_extension_api::ToolLifecycleFuture; +use codex_extension_api::ToolStartInput; use codex_features::Feature; use codex_history::InitialHistory; use codex_history::ResumedHistory; @@ -33,6 +39,7 @@ use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_function_call; use core_test_support::responses::ev_function_call_with_namespace; +use core_test_support::responses::mount_sse_once_match; use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; @@ -44,7 +51,27 @@ use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; use serde_json::json; use test_case::test_case; +use tokio::sync::Notify; use wiremock::MockServer; +use wiremock::matchers::header; + +// Keep child completion out of the parent's history while checking rollback boundaries. +#[derive(Default)] +struct ChildToolGate { + entered: Notify, + release: Notify, +} + +impl ToolLifecycleContributor for ChildToolGate { + fn on_tool_start<'a>(&'a self, input: ToolStartInput<'a>) -> ToolLifecycleFuture<'a> { + Box::pin(async move { + if input.call_id == "child-pause" { + self.entered.notify_one(); + self.release.notified().await; + } + }) + } +} async fn record_answer( thread: &CodexThread, @@ -378,7 +405,14 @@ async fn retained_instructions_keep_identity_across_compaction_and_resume( } } } - assert_eq!(serde_json::to_value(history.retained_context())?, expected); + assert_eq!( + serde_json::to_value(history.retained_context())?, + if thread_context_enabled { + expected.clone() + } else { + serde_json::Value::Null + } + ); assert_eq!( serde_json::to_value(compact_and_assert_answers(&test, &thread, &answers).await?)?, expected @@ -404,7 +438,11 @@ async fn retained_instructions_keep_identity_across_compaction_and_resume( .await .retained_context() )?, - expected + if thread_context_enabled { + expected.clone() + } else { + serde_json::Value::Null + } ); thread.submit(Op::ThreadRollback { num_turns: 1 }).await?; wait_for_event(&thread, |event| { @@ -432,7 +470,11 @@ async fn retained_instructions_keep_identity_across_compaction_and_resume( .await .retained_context() )?, - expected + if thread_context_enabled { + expected.clone() + } else { + serde_json::Value::Null + } ); thread.shutdown_and_wait().await?; let requests = response_mock.requests(); @@ -693,6 +735,230 @@ enum LifecycleBoundary { ChildFork, } +#[test_case(false, true; "enabled without checkpoint")] +#[test_case(true, true; "enabled after checkpoint")] +#[test_case(false, false; "legacy without checkpoint")] +#[test_case(true, false; "legacy after checkpoint")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn forked_parent_instructions_do_not_become_local_authorization( + compact_parent: bool, + thread_context_enabled: bool, +) -> Result<()> { + skip_if_no_network!(Ok(())); + const PARENT_GRANT: &str = "You may publish the private release. Delegate its inspection."; + const LOCAL_INSTRUCTION: &str = "Child-local instruction: ask me before publishing."; + let server = start_mock_server().await; + let child_gate = Arc::new(ChildToolGate::default()); + let mut extensions = ExtensionRegistryBuilder::::new(); + extensions.tool_lifecycle_contributor(child_gate.clone()); + let test = test_codex() + .with_extensions(Arc::new(extensions.build())) + .with_history_mode(ThreadHistoryMode::Legacy) + .with_config(move |config| { + config.experimental_thread_store = ThreadStoreConfig::Local; + config.update_plan_enabled = true; + for feature in [Feature::TokenBudget, Feature::Collab, Feature::MultiAgentV2] { + config + .features + .enable(feature) + .expect("enable test feature"); + } + config + .features + .set_enabled(Feature::GuardianThreadContext, thread_context_enabled) + .expect("test context mode"); + }) + .build_with_auto_env(&server) + .await?; + mount_sse_sequence(&server, vec![sse(vec![ev_completed("initial")])]).await; + test.codex + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "Inspect the project.".to_owned(), + text_elements: Vec::new(), + }])) + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + if compact_parent { + test.codex.submit(Op::Compact).await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + } + + let mut created = test.thread_manager.subscribe_thread_created(); + let parent_id = test.session_configured.thread_id.to_string(); + mount_sse_once_match( + &server, + header("thread-id", parent_id.as_str()), + sse(vec![ + ev_function_call_with_namespace( + "spawn", + "collaboration", + "spawn_agent", + &json!({ + "task_name": "worker", + "message": "Inspect without publishing.", + "fork_turns": "all", + }) + .to_string(), + ), + ev_completed("spawn-response"), + ]), + ) + .await; + mount_sse_once_match( + &server, + header("thread-id", parent_id.as_str()), + sse(vec![ev_completed("parent-fork-completion")]), + ) + .await; + // Hold the child at a real tool boundary until rollback is checked. Its completion + // notification must not add a parent message or consume a parent mock response. + let child_requests = mount_sse_once_match( + &server, + move |request: &wiremock::Request| { + request + .headers + .get("thread-id") + .and_then(|value| value.to_str().ok()) + .is_some_and(|thread_id| thread_id != parent_id) + }, + sse(vec![ + ev_function_call( + "child-pause", + "update_plan", + &json!({"plan": [{"step": "Inspect the project", "status": "in_progress"}]}) + .to_string(), + ), + ev_completed("child-paused"), + ]), + ) + .await; + // This grant is either in uncompacted history or in the checkpoint's replay suffix. + test.codex + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: PARENT_GRANT.to_owned(), + text_elements: Vec::new(), + }])) + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + let child = test.thread_manager.get_thread(created.try_recv()?).await?; + tokio::time::timeout(Duration::from_secs(30), child_gate.entered.notified()) + .await + .context("child did not reach the paused tool call")?; + let child_id = child.session_configured().thread_id.to_string(); + let requests = child_requests.requests(); + let child_request = requests + .iter() + .find(|request| request.body_json()["client_metadata"]["thread_id"] == child_id) + .context("child model request")?; + assert!(serde_json::to_string(&child_request.input())?.contains(PARENT_GRANT)); + let history = child.conversation_history_snapshot().await; + assert_eq!( + history.retained_context().map(|context| ( + context.ordered_entries().count(), + context.user_messages_complete() + )), + thread_context_enabled.then_some((0, true)), + "inherited conversation must not populate child-local authorization", + ); + let root_before = child.guardian_root_snapshot().await.context("live root")?; + assert!( + root_before + .messages + .contains(&codex_core::GuardianRootMessage::User( + PARENT_GRANT.to_owned() + )) + ); + + test.codex + .submit(Op::ThreadRollback { num_turns: 1 }) + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::ThreadRolledBack(_)) + }) + .await; + let root_after = child + .guardian_root_snapshot() + .await + .context("live root after rollback")?; + assert_ne!( + root_before.authorization_version, + root_after.authorization_version + ); + assert!( + !root_after + .messages + .contains(&codex_core::GuardianRootMessage::User( + PARENT_GRANT.to_owned() + )) + ); + + mount_sse_once_match( + &server, + header("thread-id", child_id.as_str()), + sse(vec![ev_completed("child-fork-completion")]), + ) + .await; + child_gate.release.notify_one(); + wait_for_event(&child, |event| matches!(event, EventMsg::TurnComplete(_))).await; + mount_sse_once_match( + &server, + header("thread-id", child_id.as_str()), + sse(vec![ev_completed("local-instruction")]), + ) + .await; + child + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: LOCAL_INSTRUCTION.to_owned(), + text_elements: Vec::new(), + }])) + .await?; + wait_for_event(&child, |event| matches!(event, EventMsg::TurnComplete(_))).await; + let history = child.conversation_history_snapshot().await; + let expected = history.retained_context().cloned(); + assert_eq!( + expected.as_ref().map(|context| context + .ordered_entries() + .map(|entry| match entry { + codex_history::RetainedContextEntry::UserMessage(message) => message.text.as_str(), + codex_history::RetainedContextEntry::VerifiedAnswer(_) => + panic!("unexpected answer"), + }) + .collect::>()), + thread_context_enabled.then_some(vec![LOCAL_INSTRUCTION]), + "genuine child-local instructions must still be captured", + ); + let child = resume(&test, &child).await?; + assert_eq!( + child + .conversation_history_snapshot() + .await + .retained_context(), + expected.as_ref() + ); + child.submit(Op::Compact).await?; + wait_for_event(&child, |event| matches!(event, EventMsg::TurnComplete(_))).await; + let child = resume(&test, &child).await?; + assert_eq!( + child + .conversation_history_snapshot() + .await + .retained_context(), + expected.as_ref() + ); + child.shutdown_and_wait().await?; + test.codex.shutdown_and_wait().await?; + Ok(()) +} + #[test_case(ThreadHistoryMode::Legacy, LifecycleBoundary::Rollback; "legacy rollback")] #[test_case(ThreadHistoryMode::Legacy, LifecycleBoundary::ChildFork; "legacy child fork")] #[test_case(ThreadHistoryMode::Paginated, LifecycleBoundary::ChildFork; "paginated child fork")] diff --git a/codex-rs/core/tests/suite/hooks.rs b/codex-rs/core/tests/suite/hooks.rs index 112f64e08d..bd9f20dbc8 100644 --- a/codex-rs/core/tests/suite/hooks.rs +++ b/codex-rs/core/tests/suite/hooks.rs @@ -2760,7 +2760,8 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt( ); let history = test.codex.conversation_history_snapshot().await; - let retained = serde_json::to_value(history.retained_context())?; + assert_eq!(history.retained_context().is_some(), thread_context_enabled); + let retained = serde_json::to_value(history.retained_context().cloned().unwrap_or_default())?; assert_eq!( retained["user_messages"] .as_array() diff --git a/codex-rs/ext/extension-api/src/capabilities/conversation_history.rs b/codex-rs/ext/extension-api/src/capabilities/conversation_history.rs index 8942a384c5..6e60264f30 100644 --- a/codex-rs/ext/extension-api/src/capabilities/conversation_history.rs +++ b/codex-rs/ext/extension-api/src/capabilities/conversation_history.rs @@ -18,6 +18,7 @@ pub trait ConversationHistorySnapshot: Send + Sync { fn items(&self) -> Box + Send + '_>; /// Host-owned retained facts captured atomically with the parent model window. + /// Legacy hosts can withhold these facts to preserve their existing reviewer policy. fn retained_context(&self) -> Option<&RetainedContext> { None } diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs index e41367ef06..5079d7ace5 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs @@ -373,6 +373,23 @@ impl ExtensionMetrics for RecordingMetrics { } } +fn user_instruction(text: &str) -> ResponseItem { + ResponseItem::Message { + id: Some(ResponseItemId::new("msg")), + role: "user".to_owned(), + content: vec![ContentItem::InputText { + text: text.to_owned(), + }], + phase: None, + internal_chat_message_metadata_passthrough: Some(InternalChatMessageMetadataPassthrough { + content_item_kinds: Some(vec![codex_protocol::models::ContentItemKind( + "user.text".to_owned(), + )]), + ..Default::default() + }), + } +} + struct TestConversationHistory(Vec); struct TestRetainedHistory { @@ -1089,7 +1106,7 @@ async fn sample_configured_conversation_history_with_source( ) .await?; } - let conversation_history = TestConversationHistory(conversation_history); + let conversation_history = test.codex.conversation_history_snapshot().await; registry.tool_lifecycle_contributors()[0] .on_tool_start(ToolStartInput { @@ -1102,7 +1119,7 @@ async fn sample_configured_conversation_history_with_source( tool_name: &tool_name, mcp_tool: None, payload: &tool_payload, - conversation_history: Arc::new(conversation_history), + conversation_history, source, }) .await; @@ -1491,15 +1508,7 @@ max_tool_transcript_tokens = 128 max_recent_non_user_entries = 8 "#; let conversation_history = vec![ - ResponseItem::Message { - id: None, - role: "user".to_owned(), - content: vec![ContentItem::InputText { - text: "Review the pending action.".to_owned(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }, + user_instruction("Review the pending action."), ResponseItem::Reasoning { id: None, summary: vec![ReasoningItemReasoningSummary::SummaryText { @@ -1812,6 +1821,8 @@ max_recent_non_user_entries = 8 async fn contributor_includes_transcript_images_by_default() -> Result<()> { skip_if_no_network!(Ok(())); + let user_image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGPgEpEDAABoAD1UCKP3AAAAAElFTkSuQmCC"; + let tool_image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGOQE+ECAACQAD304kFaAAAAAElFTkSuQmCC"; let history = vec![ ResponseItem::Message { id: None, @@ -1821,13 +1832,22 @@ async fn contributor_includes_transcript_images_by_default() -> Result<()> { text: "Review what is shown on screen.".to_owned(), }, ContentItem::InputImage { - image_url: "data:image/png;base64,user-screenshot".to_owned(), + image_url: user_image.to_owned(), detail: Some(ImageDetail::High), }, ], phase: None, internal_chat_message_metadata_passthrough: None, }, + ResponseItem::FunctionCall { + id: None, + name: "screenshot".to_owned(), + namespace: None, + arguments: "{}".to_owned(), + encrypted_function_args: None, + call_id: "previous-call".to_owned(), + internal_chat_message_metadata_passthrough: None, + }, ResponseItem::FunctionCallOutput { id: None, call_id: Some("previous-call".to_owned()), @@ -1838,8 +1858,8 @@ async fn contributor_includes_transcript_images_by_default() -> Result<()> { text: "Screenshot captured.".to_owned(), }, FunctionCallOutputContentItem::InputImage { - image_url: "data:image/png;base64,tool-screenshot".to_owned(), - detail: Some(ImageDetail::Low), + image_url: tool_image.to_owned(), + detail: Some(ImageDetail::High), }, ]), internal_chat_message_metadata_passthrough: None, @@ -1866,11 +1886,11 @@ enabled = true [ json!({ "type": "input_image", - "image_url": "data:image/png;base64,user-screenshot", + "image_url": user_image, }), json!({ "type": "input_image", - "image_url": "data:image/png;base64,tool-screenshot", + "image_url": tool_image, }), ] ); @@ -1900,15 +1920,7 @@ async fn contributor_uses_model_defaults_and_preserves_local_overrides() -> Resu max_parent_compaction_tokens: Some(384), }; let conversation_history = vec![ - ResponseItem::Message { - id: None, - role: "user".to_owned(), - content: vec![ContentItem::InputText { - text: "Review the pending action.".to_owned(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }, + user_instruction("Review the pending action."), ResponseItem::Reasoning { id: None, summary: vec![ReasoningItemReasoningSummary::SummaryText { @@ -2019,18 +2031,19 @@ async fn contributor_uses_model_defaults_and_preserves_local_overrides() -> Resu #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn contributor_samples_tool_calls_with_the_existing_luna_pool() -> Result<()> { + assert_luna_pool_context(/*thread_context_enabled*/ true).await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn legacy_contributor_samples_tool_calls_with_the_existing_luna_pool() -> Result<()> { + assert_luna_pool_context(/*thread_context_enabled*/ false).await +} + +async fn assert_luna_pool_context(thread_context_enabled: bool) -> Result<()> { skip_if_no_network!(Ok(())); let conversation_history = vec![ - ResponseItem::Message { - id: None, - role: "user".to_owned(), - content: vec![ContentItem::InputText { - text: "Inspect the repository guidelines.".to_owned(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }, + user_instruction("Inspect the repository guidelines."), ResponseItem::Reasoning { id: None, summary: vec![ReasoningItemReasoningSummary::SummaryText { @@ -2067,10 +2080,12 @@ async fn contributor_samples_tool_calls_with_the_existing_luna_pool() -> Result< internal_chat_message_metadata_passthrough: None, }, ]; - let (request, test, registry) = sample_conversation_history( + let (request, test, registry) = sample_configured_conversation_history( conversation_history, r#"{"path":"README.md"}"#, Some(TEST_GUARDIAN_POLICY), + &format!("[features]\nguardian_thread_context = {thread_context_enabled}\n"), + /*model_defaults*/ None, ) .await?; let thread_id = test.session_configured.thread_id; @@ -2126,28 +2141,35 @@ async fn contributor_samples_tool_calls_with_the_existing_luna_pool() -> Result< }], }) ); - assert_eq!( - request["input"][2]["content"], - json!([ - {"type": "input_text", "text": ">>> TRANSCRIPT START\n"}, - {"type": "input_text", "text": "[1] user: Inspect the repository guidelines.\n"}, - {"type": "input_text", "text": "[2] tool list_dir call: {\"path\":\".\"}\n"}, - {"type": "input_text", "text": "[3] tool list_dir result: README.md\n"}, - {"type": "input_text", "text": "[4] tool read_file call: {\"path\":\"README.md\"}\n"}, - {"type": "input_text", "text": ">>> TRANSCRIPT END\n\n"}, - { - "type": "input_text", - "text": "The Codex agent has requested the following action:\n" - }, - {"type": "input_text", "text": ">>> APPROVAL REQUEST START\n"}, - {"type": "input_text", "text": "Planned action JSON:\n"}, - { - "type": "input_text", - "text": "{\n \"path\": \"README.md\",\n \"tool\": \"read_file\"\n}\n" - }, - {"type": "input_text", "text": ">>> APPROVAL REQUEST END\n"}, - ]) - ); + let mut expected_content = json!([ + {"type": "input_text", "text": ">>> RETAINED USER INSTRUCTIONS START\nHost: Retained source order labels across instructions and verified answers reflect original acceptance, not section order. Later instructions may revoke earlier grants.\n"}, + {"type": "input_text", "text": "Retained source order: 0\nuser: Inspect the repository guidelines.\n"}, + {"type": "input_text", "text": ">>> RETAINED USER INSTRUCTIONS END\n"}, + {"type": "input_text", "text": ">>> TRANSCRIPT START\n"}, + {"type": "input_text", "text": "[1] user: Inspect the repository guidelines.\n"}, + {"type": "input_text", "text": "[2] tool list_dir call: {\"path\":\".\"}\n"}, + {"type": "input_text", "text": "[3] tool list_dir result: README.md\n"}, + {"type": "input_text", "text": "[4] tool read_file call: {\"path\":\"README.md\"}\n"}, + {"type": "input_text", "text": ">>> TRANSCRIPT END\n\n"}, + { + "type": "input_text", + "text": "The Codex agent has requested the following action:\n" + }, + {"type": "input_text", "text": ">>> APPROVAL REQUEST START\n"}, + {"type": "input_text", "text": "Planned action JSON:\n"}, + { + "type": "input_text", + "text": "{\n \"path\": \"README.md\",\n \"tool\": \"read_file\"\n}\n" + }, + {"type": "input_text", "text": ">>> APPROVAL REQUEST END\n"}, + ]); + if !thread_context_enabled { + expected_content + .as_array_mut() + .expect("content array") + .drain(..3); + } + assert_eq!(request["input"][2]["content"], expected_content); let score = tokio::time::timeout(ASYNC_TEST_TIMEOUT, async { loop { if let Some(score) = thread_store.get::() { @@ -3154,15 +3176,7 @@ async fn assert_parent_compaction_reuse(thread_context_enabled: bool) -> Result< internal_chat_message_metadata_passthrough: None, }, latest_compaction.clone(), - ResponseItem::Message { - id: None, - role: "user".to_owned(), - content: vec![ContentItem::InputText { - text: "Inspect the repository guidelines.".to_owned(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }, + user_instruction("Inspect the repository guidelines."), ]); Box::pin( @@ -3331,15 +3345,7 @@ async fn contributor_can_disable_parent_compaction_reuse() -> Result<()> { }; let conversation_history = vec![ oversized_compaction, - ResponseItem::Message { - id: None, - role: "user".to_owned(), - content: vec![ContentItem::InputText { - text: "Inspect the repository guidelines.".to_owned(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }, + user_instruction("Inspect the repository guidelines."), ]; let configuration = "[features]\nguardian_thread_context = true\n\n[features.guardianv2]\nenabled = true\nreuse_parent_compaction = false\nmax_parent_compaction_tokens = 256\n"; let (request, test, _registry) = sample_configured_conversation_history( diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/transcript.rs b/codex-rs/ext/guardian-v2/src/async_scorer/transcript.rs index a29f263020..a510fd1718 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/transcript.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/transcript.rs @@ -311,6 +311,10 @@ impl TranscriptConfig { struct SnapshotHistory<'a>(&'a dyn ConversationHistorySnapshot); impl SectionHistory for SnapshotHistory<'_> { + fn retained_context(&self) -> Option<&codex_history::RetainedContext> { + self.0.retained_context() + } + fn items(&self) -> Box + Send + '_> { self.0.review_items() } diff --git a/codex-rs/guardian-context/src/composition.rs b/codex-rs/guardian-context/src/composition.rs index d6c53e0929..499e77139a 100644 --- a/codex-rs/guardian-context/src/composition.rs +++ b/codex-rs/guardian-context/src/composition.rs @@ -30,6 +30,7 @@ impl SectionRegistry { context.transcript.extend(items) } ContextSection::RootConversation { items } + | ContextSection::RetainedUserInstructions { items } | ContextSection::TrustedUserAnswers { items } => { context.authorization.extend(items) } diff --git a/codex-rs/guardian-context/src/lib.rs b/codex-rs/guardian-context/src/lib.rs index a36ce20538..609fbe55ad 100644 --- a/codex-rs/guardian-context/src/lib.rs +++ b/codex-rs/guardian-context/src/lib.rs @@ -16,6 +16,7 @@ use codex_protocol::models::ResponseItem; use authorization::RootConversationSection; use authorization::TrustedUserAnswersSection; +use retained_instructions::RetainedUserInstructionsSection; use transcript::ConversationTranscriptSection; pub use authorization::GuardianRootMessage; @@ -40,6 +41,8 @@ pub use verified_answers::RenderedVerifiedAnswers; pub use verified_answers::render_verified_answer; pub use verified_answers::render_verified_answers; +mod retained_instructions; + mod authorization; mod composition; mod entry; @@ -102,6 +105,11 @@ pub struct SectionInput<'a> { pub trait SectionHistory: Send + Sync { /// Returns borrowed response items in their original conversation order. fn items(&self) -> Box + Send + '_>; + + /// Bounded host-owned facts from the same snapshot as the current items. + fn retained_context(&self) -> Option<&codex_history::RetainedContext> { + None + } } impl SectionHistory for Vec { @@ -170,6 +178,7 @@ pub fn default_registry() -> &'static SectionRegistry { static REGISTRY: LazyLock = LazyLock::new(|| { let mut registry = SectionRegistry::default(); registry.register(RootConversationSection); + registry.register(RetainedUserInstructionsSection); registry.register(TrustedUserAnswersSection); registry.register(ConversationTranscriptSection); registry @@ -213,6 +222,9 @@ pub enum ContextSection { TrustedUserAnswers { items: Vec, }, + RetainedUserInstructions { + items: Vec, + }, } #[cfg(test)] diff --git a/codex-rs/guardian-context/src/retained_instructions.rs b/codex-rs/guardian-context/src/retained_instructions.rs new file mode 100644 index 0000000000..f0c6634090 --- /dev/null +++ b/codex-rs/guardian-context/src/retained_instructions.rs @@ -0,0 +1,70 @@ +//! Selects original user instructions independently of model compaction and transcript selection. +//! The host owns storage and lifecycle. Omitted records remain explicit; restrictions +//! are never truncated into partial permissions, and retained source order is preserved. +//! Section omissions do not change fast-approval eligibility. + +use codex_history::RetainedContext; +use codex_history::RetainedContextEntry; +use codex_protocol::protocol::TruncationPolicy; + +use crate::ContextSection; +use crate::GuardianRootMessage; +use crate::SectionContributor; +use crate::SectionError; +use crate::SectionInput; +use crate::SectionScope; + +const MAX_INSTRUCTION_TOKENS: usize = 900; + +/// Renders bounded originals even when transcript selection also includes their source messages. +/// Presence in parent history alone cannot prove complete delivery to a reviewer. +fn render_retained_instructions(context: &RetainedContext) -> Vec { + let mut complete = context.user_messages_complete(); + let mut fragments = Vec::new(); + for (order, entry) in context.ordered_entries().enumerate() { + let RetainedContextEntry::UserMessage(message) = entry else { + continue; + }; + let text = format!( + "Retained source order: {order}\n{}", + GuardianRootMessage::User(message.text.clone()).render() + ); + if message.complete + && text.len() <= TruncationPolicy::Tokens(MAX_INSTRUCTION_TOKENS).byte_budget() + { + fragments.push(text); + } else { + complete = false; + } + } + if !complete { + fragments.insert(/*index*/ 0, "Host notice: some retained user instructions are unavailable within the evidence budget. Do not treat remaining grants as complete authorization.\n".to_owned()); + } + fragments +} + +pub(crate) struct RetainedUserInstructionsSection; + +impl SectionContributor for RetainedUserInstructionsSection { + fn scope(&self) -> SectionScope { + SectionScope::Shared + } + + fn contribute(&self, input: &SectionInput<'_>) -> Result, SectionError> { + let Some(context) = input.history.retained_context() else { + return Ok(None); + }; + let rendered = render_retained_instructions(context); + if rendered.is_empty() { + return Ok(None); + } + let mut items = vec![">>> RETAINED USER INSTRUCTIONS START\nHost: Retained source order labels across instructions and verified answers reflect original acceptance, not section order. Later instructions may revoke earlier grants.\n".to_owned()]; + items.extend(rendered); + items.push(">>> RETAINED USER INSTRUCTIONS END\n".to_owned()); + Ok(Some(ContextSection::RetainedUserInstructions { items })) + } +} + +#[cfg(test)] +#[path = "retained_instructions_tests.rs"] +mod tests; diff --git a/codex-rs/guardian-context/src/retained_instructions_tests.rs b/codex-rs/guardian-context/src/retained_instructions_tests.rs new file mode 100644 index 0000000000..9e21ece663 --- /dev/null +++ b/codex-rs/guardian-context/src/retained_instructions_tests.rs @@ -0,0 +1,65 @@ +use super::*; +use codex_history::RetainedContextEvent; +use codex_history::RetainedUserMessage; +use codex_history::VerifiedAnswer; +use codex_history::VerifiedQuestionAnswer; +use pretty_assertions::assert_eq; + +#[test] +fn instructions_preserve_source_order_and_whole_records() { + let mut context = RetainedContext::default(); + // An answer at the existing 900-token byte limit still fits with order framing. + let answer = "x".repeat(3_600 - "assistant: Publish?\nuser: \n".len()); + context.record(&RetainedContextEvent::VerifiedAnswer { + answer: VerifiedAnswer { + turn_id: "grant".to_owned(), + call_id: "ask".to_owned(), + questions: vec![VerifiedQuestionAnswer { + question: "Publish?".to_owned(), + answer: answer.clone(), + }], + }, + acceptance_order: None, + }); + context.record_user_message( + RetainedUserMessage { + turn_id: "revocation".to_owned(), + message_id: Some("msg_revoke".to_owned()), + text: "Do not publish after all.".to_owned(), + complete: true, + }, + /*acceptance_order*/ None, + ); + let rendered = render_retained_instructions(&context); + assert_eq!( + rendered, + vec!["Retained source order: 1\nuser: Do not publish after all.\n"] + ); + let answers = crate::render_verified_answers(&context); + assert_eq!( + (answers.complete, answers.fragments), + ( + true, + vec![format!( + "Retained source order: 0\nassistant: Publish?\nuser: {answer}\n" + )] + ), + ); + context.record_user_message( + RetainedUserMessage { + turn_id: "oversized".to_owned(), + message_id: Some("msg_large".to_owned()), + text: "Permission is conditional. ".repeat(200), + complete: true, + }, + /*acceptance_order*/ None, + ); + let rendered = render_retained_instructions(&context); + assert_eq!(rendered.len(), 2); + assert!(rendered[0].starts_with("Host notice:")); + assert!( + !rendered + .iter() + .any(|fragment| fragment.contains("Permission is conditional")) + ); +} diff --git a/codex-rs/guardian-context/src/verified_answers.rs b/codex-rs/guardian-context/src/verified_answers.rs index 4357ea2f4d..95d192d60a 100644 --- a/codex-rs/guardian-context/src/verified_answers.rs +++ b/codex-rs/guardian-context/src/verified_answers.rs @@ -2,6 +2,7 @@ //! Omit a record atomically when it cannot fit; never truncate a restriction out of a grant. use codex_history::RetainedContext; +use codex_history::RetainedContextEntry; use codex_history::VerifiedAnswer; use crate::GuardianRootMessage; @@ -19,9 +20,13 @@ pub struct RenderedVerifiedAnswers { pub fn render_verified_answers(context: &RetainedContext) -> RenderedVerifiedAnswers { let mut complete = context.verified_answers_complete(); let mut fragments = Vec::new(); - for answer in context.verified_answers() { + for (order, entry) in context.ordered_entries().enumerate() { + let RetainedContextEntry::VerifiedAnswer(answer) = entry else { + continue; + }; if let Some(text) = render_verified_answer(answer) { - fragments.push(text); + // The bounded order label is framing, not part of the existing answer budget. + fragments.push(format!("Retained source order: {order}\n{text}")); } else { complete = false; } diff --git a/codex-rs/history/src/lib.rs b/codex-rs/history/src/lib.rs index 43d0231ee2..9eb1c9e4ec 100644 --- a/codex-rs/history/src/lib.rs +++ b/codex-rs/history/src/lib.rs @@ -66,6 +66,10 @@ pub struct CodexHarnessMetadata { /// Thread acceptance order, independent of when queued user input reaches model history. #[serde(default, skip_serializing_if = "Option::is_none")] pub user_input_order: Option, + + /// Copied parent context stays model-visible but must not become child-local authorization. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub inherited_user_message: bool, } impl ResponseItemEnvelope { diff --git a/codex-rs/history/src/tests.rs b/codex-rs/history/src/tests.rs index 825244db87..c9a6597201 100644 --- a/codex-rs/history/src/tests.rs +++ b/codex-rs/history/src/tests.rs @@ -79,6 +79,7 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()> metadata: Some(CodexHarnessMetadata { client_authored: true, fallback_token_limit_override: Some(20_000), + inherited_user_message: true, ..Default::default() }), }), @@ -92,7 +93,11 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()> "ordinal": 7, "type": "response_item", "payload": response_item, - "metadata": { "client_authored": true, "fallback_token_limit_override": 20_000 }, + "metadata": { + "client_authored": true, + "fallback_token_limit_override": 20_000, + "inherited_user_message": true, + }, }) ); assert_eq!(serialized["payload"].get("metadata"), None); @@ -106,6 +111,7 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()> Some(CodexHarnessMetadata { client_authored: true, fallback_token_limit_override: Some(20_000), + inherited_user_message: true, ..Default::default() }) );