diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 37a64fbe33..7870132d22 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -533,6 +533,13 @@ } ] }, + "ContextMode": { + "enum": [ + "default", + "vanilla" + ], + "type": "string" + }, "DeviceKeyCreateParams": { "description": "Create a controller-local device key with a random key id.", "properties": { @@ -3457,6 +3464,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", @@ -3861,6 +3878,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", @@ -4039,6 +4066,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index f856b43d66..02334ef9ed 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -7820,6 +7820,13 @@ "title": "ContextCompactedNotification", "type": "object" }, + "ContextMode": { + "enum": [ + "default", + "vanilla" + ], + "type": "string" + }, "CreditsSnapshot": { "properties": { "balance": { @@ -15123,6 +15130,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", @@ -16623,6 +16640,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", @@ -16912,6 +16939,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index c17efe7a45..ad5dd77b16 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -4318,6 +4318,13 @@ "title": "ContextCompactedNotification", "type": "object" }, + "ContextMode": { + "enum": [ + "default", + "vanilla" + ], + "type": "string" + }, "CreditsSnapshot": { "properties": { "balance": { @@ -13009,6 +13016,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", @@ -14509,6 +14526,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", @@ -14798,6 +14825,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index 970e2fe9ca..ea8db649b4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -64,6 +64,13 @@ } ] }, + "ContextMode": { + "enum": [ + "default", + "vanilla" + ], + "type": "string" + }, "PermissionProfileModificationParams": { "oneOf": [ { @@ -175,6 +182,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index cfe6509195..fa355a967b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -138,6 +138,13 @@ } ] }, + "ContextMode": { + "enum": [ + "default", + "vanilla" + ], + "type": "string" + }, "FunctionCallOutputBody": { "anyOf": [ { @@ -1033,6 +1040,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index d5f0e9bfcc..d6fac3c899 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -64,6 +64,13 @@ } ] }, + "ContextMode": { + "enum": [ + "default", + "vanilla" + ], + "type": "string" + }, "DynamicToolSpec": { "properties": { "deferLoading": { @@ -230,6 +237,16 @@ "null" ] }, + "contextMode": { + "anyOf": [ + { + "$ref": "#/definitions/ContextMode" + }, + { + "type": "null" + } + ] + }, "cwd": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ContextMode.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ContextMode.ts new file mode 100644 index 0000000000..7e652a38a7 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ContextMode.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ContextMode = "default" | "vanilla"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts index ba7119e9ed..640f9e062c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts @@ -5,6 +5,7 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { ContextMode } from "./ContextMode"; import type { SandboxMode } from "./SandboxMode"; /** @@ -23,4 +24,4 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean}; +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, contextMode?: ContextMode | null, ephemeral?: boolean}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts index ac8b1e293b..dc8fe66b2c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts @@ -6,6 +6,7 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { ContextMode } from "./ContextMode"; import type { SandboxMode } from "./SandboxMode"; /** @@ -26,4 +27,4 @@ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null}; +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, contextMode?: ContextMode | null, personality?: Personality | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts index 374ac2e681..1247f328de 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts @@ -6,6 +6,7 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; +import type { ContextMode } from "./ContextMode"; import type { SandboxMode } from "./SandboxMode"; import type { ThreadStartSource } from "./ThreadStartSource"; @@ -13,4 +14,4 @@ export type ThreadStartParams = {model?: string | null, modelProvider?: string | * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null}; +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, contextMode?: ContextMode | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index d369ba3423..c25812b0dd 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -77,6 +77,7 @@ export type { ConfigWriteResponse } from "./ConfigWriteResponse"; export type { ConfiguredHookHandler } from "./ConfiguredHookHandler"; export type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup"; export type { ContextCompactedNotification } from "./ContextCompactedNotification"; +export type { ContextMode } from "./ContextMode"; export type { CreditsSnapshot } from "./CreditsSnapshot"; export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotification"; export type { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 963ac69000..63c3c39824 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -21,6 +21,7 @@ use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleA use codex_protocol::config_types::ApprovalsReviewer as CoreApprovalsReviewer; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationModeMask as CoreCollaborationModeMask; +use codex_protocol::config_types::ContextMode as CoreContextMode; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; @@ -414,6 +415,13 @@ impl From for SandboxMode { } } +v2_enum_from_core!( + pub enum ContextMode from CoreContextMode { + Default, + Vanilla + } +); + v2_enum_from_core!( pub enum ReviewDelivery from codex_protocol::protocol::ReviewDelivery { Inline, Detached @@ -3584,6 +3592,8 @@ pub struct ThreadStartParams { #[ts(optional = nullable)] pub developer_instructions: Option, #[ts(optional = nullable)] + pub context_mode: Option, + #[ts(optional = nullable)] pub personality: Option, #[ts(optional = nullable)] pub ephemeral: Option, @@ -3735,6 +3745,8 @@ pub struct ThreadResumeParams { #[ts(optional = nullable)] pub developer_instructions: Option, #[ts(optional = nullable)] + pub context_mode: Option, + #[ts(optional = nullable)] pub personality: Option, /// When true, return only thread metadata and live-resume state without /// populating `thread.turns`. This is useful when the client plans to call @@ -3839,6 +3851,8 @@ pub struct ThreadForkParams { pub base_instructions: Option, #[ts(optional = nullable)] pub developer_instructions: Option, + #[ts(optional = nullable)] + pub context_mode: Option, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub ephemeral: bool, /// When true, return only thread metadata and live fork state without diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index edc142c840..0f9759c507 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -262,6 +262,7 @@ use codex_core::ThreadConfigSnapshot; use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::config::ContextMode as CoreContextMode; use codex_core::config::NetworkProxyAuditMetadata; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; @@ -706,6 +707,9 @@ fn environment_selection_error_message(err: CodexErr) -> String { impl CodexMessageProcessor { async fn instruction_sources_from_config(config: &Config) -> Vec { + if !config.context_mode.includes_context_enrichment() { + return Vec::new(); + } codex_core::AgentsMdManager::new(config) .instruction_sources(LOCAL_FS.as_ref()) .await @@ -2510,6 +2514,7 @@ impl CodexMessageProcessor { service_name, base_instructions, developer_instructions, + context_mode, dynamic_tools, mock_experimental_field: _mock_experimental_field, experimental_raw_events, @@ -2546,6 +2551,7 @@ impl CodexMessageProcessor { permissions, base_instructions, developer_instructions, + context_mode, personality, ); typesafe_overrides.ephemeral = ephemeral; @@ -2605,6 +2611,7 @@ impl CodexMessageProcessor { /*permissions*/ None, /*base_instructions*/ None, /*developer_instructions*/ None, + /*context_mode*/ None, /*personality*/ None, ); let config = self @@ -2975,6 +2982,7 @@ impl CodexMessageProcessor { permissions: Option, base_instructions: Option, developer_instructions: Option, + context_mode: Option, personality: Option, ) -> ConfigOverrides { let mut overrides = ConfigOverrides { @@ -2991,6 +2999,7 @@ impl CodexMessageProcessor { main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(), base_instructions, developer_instructions, + context_mode: context_mode.map(core_context_mode), personality, ..Default::default() }; @@ -4355,6 +4364,7 @@ impl CodexMessageProcessor { config: mut request_overrides, base_instructions, developer_instructions, + context_mode, personality, exclude_turns, persist_extended_history, @@ -4389,6 +4399,7 @@ impl CodexMessageProcessor { permissions, base_instructions, developer_instructions, + context_mode, personality, ); self.load_and_apply_persisted_resume_metadata( @@ -4397,6 +4408,7 @@ impl CodexMessageProcessor { &mut typesafe_overrides, ) .await; + apply_context_mode_from_history(&thread_history, &mut typesafe_overrides); // Derive a Config using the same logic as new conversation, honoring overrides if provided. let config = match self @@ -4599,7 +4611,7 @@ impl CodexMessageProcessor { if let (Some(requested_path), Some(active_path)) = ( params.path.as_ref(), existing_thread.rollout_path().as_ref(), - ) && requested_path != active_path + ) && !path_utils::paths_match_after_normalization(requested_path, active_path) { return Err(invalid_request(format!( "cannot resume running thread {existing_thread_id} with stale path: requested `{}`, active `{}`", @@ -4957,6 +4969,7 @@ impl CodexMessageProcessor { config: cli_overrides, base_instructions, developer_instructions, + context_mode, ephemeral, exclude_turns, persist_extended_history, @@ -5021,9 +5034,11 @@ impl CodexMessageProcessor { permissions, base_instructions, developer_instructions, + context_mode, /*personality*/ None, ); typesafe_overrides.ephemeral = ephemeral.then_some(true); + apply_context_mode_from_items(&history_items, &mut typesafe_overrides); // Derive a Config using the same logic as new conversation, honoring overrides if provided. let config = self .config_manager @@ -8748,6 +8763,10 @@ fn collect_resume_override_mismatches( "developerInstructions override was provided and ignored while running".to_string(), ); } + if request.context_mode.is_some() { + mismatch_details + .push("contextMode override was provided and ignored while running".to_string()); + } if request.persist_extended_history { mismatch_details.push( "persistExtendedHistory override was provided and ignored while running".to_string(), @@ -8777,6 +8796,44 @@ fn merge_persisted_resume_metadata( } } +fn apply_context_mode_from_history( + thread_history: &InitialHistory, + typesafe_overrides: &mut ConfigOverrides, +) { + if typesafe_overrides.context_mode.is_none() + && let Some(context_mode) = thread_history.get_context_mode() + { + typesafe_overrides.context_mode = Some(core_context_mode_from_protocol(context_mode)); + } +} + +fn apply_context_mode_from_items( + history_items: &[RolloutItem], + typesafe_overrides: &mut ConfigOverrides, +) { + if typesafe_overrides.context_mode.is_none() + && let Some(context_mode) = history_items.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.context_mode), + _ => None, + }) + { + typesafe_overrides.context_mode = Some(core_context_mode_from_protocol(context_mode)); + } +} + +fn core_context_mode(mode: codex_app_server_protocol::ContextMode) -> CoreContextMode { + core_context_mode_from_protocol(mode.to_core()) +} + +fn core_context_mode_from_protocol( + mode: codex_protocol::config_types::ContextMode, +) -> CoreContextMode { + match mode { + codex_protocol::config_types::ContextMode::Default => CoreContextMode::Default, + codex_protocol::config_types::ContextMode::Vanilla => CoreContextMode::Vanilla, + } +} + fn has_model_resume_override( request_overrides: Option<&HashMap>, typesafe_overrides: &ConfigOverrides, @@ -10537,6 +10594,7 @@ mod tests { config: None, base_instructions: None, developer_instructions: None, + context_mode: None, personality: None, exclude_turns: false, persist_extended_history: false, @@ -10553,6 +10611,7 @@ mod tests { ephemeral: false, reasoning_effort: None, personality: None, + context_mode: CoreContextMode::Default, session_source: SessionSource::Cli, }; @@ -10562,6 +10621,95 @@ mod tests { ); } + #[test] + fn collect_resume_override_mismatches_includes_context_mode() { + let cwd = test_path_buf("/tmp").abs(); + let request = ThreadResumeParams { + thread_id: "thread-1".to_string(), + context_mode: Some(codex_app_server_protocol::ContextMode::Vanilla), + ..ThreadResumeParams::default() + }; + let config_snapshot = ThreadConfigSnapshot { + model: "gpt-5".to_string(), + model_provider_id: "openai".to_string(), + service_tier: None, + approval_policy: codex_protocol::protocol::AskForApproval::OnRequest, + approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, + permission_profile: codex_protocol::models::PermissionProfile::Disabled, + active_permission_profile: None, + cwd, + ephemeral: false, + reasoning_effort: None, + personality: None, + context_mode: CoreContextMode::Default, + session_source: SessionSource::Cli, + }; + + assert_eq!( + collect_resume_override_mismatches(&request, &config_snapshot), + vec!["contextMode override was provided and ignored while running".to_string()] + ); + } + + #[test] + fn apply_context_mode_from_history_uses_stored_mode_when_omitted() { + let thread_id = ThreadId::new(); + let thread_history = InitialHistory::Resumed(codex_protocol::protocol::ResumedHistory { + conversation_id: thread_id, + history: vec![session_meta_item( + codex_protocol::config_types::ContextMode::Vanilla, + )], + rollout_path: None, + }); + let mut overrides = ConfigOverrides::default(); + + apply_context_mode_from_history(&thread_history, &mut overrides); + + assert_eq!(overrides.context_mode, Some(CoreContextMode::Vanilla)); + } + + #[test] + fn apply_context_mode_from_history_preserves_explicit_mode() { + let thread_id = ThreadId::new(); + let thread_history = InitialHistory::Resumed(codex_protocol::protocol::ResumedHistory { + conversation_id: thread_id, + history: vec![session_meta_item( + codex_protocol::config_types::ContextMode::Vanilla, + )], + rollout_path: None, + }); + let mut overrides = ConfigOverrides { + context_mode: Some(CoreContextMode::Default), + ..Default::default() + }; + + apply_context_mode_from_history(&thread_history, &mut overrides); + + assert_eq!(overrides.context_mode, Some(CoreContextMode::Default)); + } + + #[test] + fn apply_context_mode_from_items_uses_stored_mode_when_omitted() { + let history_items = vec![session_meta_item( + codex_protocol::config_types::ContextMode::Vanilla, + )]; + let mut overrides = ConfigOverrides::default(); + + apply_context_mode_from_items(&history_items, &mut overrides); + + assert_eq!(overrides.context_mode, Some(CoreContextMode::Vanilla)); + } + + fn session_meta_item(context_mode: codex_protocol::config_types::ContextMode) -> RolloutItem { + RolloutItem::SessionMeta(SessionMetaLine { + meta: SessionMeta { + context_mode, + ..SessionMeta::default() + }, + git: None, + }) + } + fn test_thread_metadata( model: Option<&str>, reasoning_effort: Option, diff --git a/codex-rs/app-server/tests/common/rollout.rs b/codex-rs/app-server/tests/common/rollout.rs index 06b273754c..bcc7511e2a 100644 --- a/codex-rs/app-server/tests/common/rollout.rs +++ b/codex-rs/app-server/tests/common/rollout.rs @@ -142,6 +142,7 @@ pub fn create_fake_rollout_with_source( agent_nickname: None, agent_role: None, model_provider: model_provider.map(str::to_string), + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, @@ -225,6 +226,7 @@ pub fn create_fake_rollout_with_text_elements( agent_nickname: None, agent_role: None, model_provider: model_provider.map(str::to_string), + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index b95adb9044..48acc6d135 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -672,6 +672,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<( service_name: None, base_instructions: None, developer_instructions: None, + context_mode: None, personality: None, ephemeral: None, session_start_source: None, diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index 589c7c330a..4b98c57f69 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -1115,6 +1115,7 @@ async fn seed_pathless_store_thread( metadata: ThreadPersistenceMetadata { cwd: None, model_provider: "test-provider".to_string(), + context_mode: codex_protocol::config_types::ContextMode::Default, memory_mode: ThreadMemoryMode::Disabled, }, event_persistence_mode: ThreadEventPersistenceMode::default(), diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 48673387b8..8a14ab292a 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -1182,6 +1182,7 @@ stream_max_retries = 0 agent_nickname: None, agent_role: None, model_provider: Some("mock_provider".to_string()), + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index cc83c0a7c1..6f7f54b5db 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -1,5 +1,6 @@ use crate::agent::AgentStatus; use crate::config::ConstraintResult; +use crate::config::ContextMode; use crate::file_watcher::WatchRegistration; use crate::goals::GoalRuntimeEvent; use crate::session::Codex; @@ -58,6 +59,7 @@ pub struct ThreadConfigSnapshot { pub ephemeral: bool, pub reasoning_effort: Option, pub personality: Option, + pub context_mode: ContextMode, pub session_source: SessionSource, } diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 1352de991e..3871bc02e0 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4851,6 +4851,47 @@ async fn cli_override_sets_compact_prompt() -> std::io::Result<()> { Ok(()) } +#[tokio::test] +async fn vanilla_context_mode_disables_context_enrichment_config() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cfg = toml::from_str::( + r#" +instructions = "CFG_BASE_INSTRUCTIONS_MARKER" +developer_instructions = "CFG_DEVELOPER_INSTRUCTIONS_MARKER" +include_apps_instructions = true +include_environment_context = true + +[skills] +include_instructions = true +"#, + ) + .expect("config TOML should parse"); + + let config = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides { + base_instructions: Some("OVERRIDE_BASE_INSTRUCTIONS_MARKER".to_string()), + developer_instructions: Some("OVERRIDE_DEVELOPER_INSTRUCTIONS_MARKER".to_string()), + context_mode: Some(ContextMode::Vanilla), + personality: Some(Personality::Pragmatic), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + assert_eq!(config.context_mode, ContextMode::Vanilla); + assert_eq!(config.base_instructions, None); + assert_eq!(config.developer_instructions, None); + assert_eq!(config.personality, None); + assert!(config.include_permissions_instructions); + assert!(!config.include_apps_instructions); + assert!(!config.include_skill_instructions); + assert!(!config.include_environment_context); + + Ok(()) +} + #[tokio::test] async fn loads_compact_prompt_from_file() -> std::io::Result<()> { let codex_home = TempDir::new()?; @@ -6422,6 +6463,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { experimental_thread_store: ThreadStoreConfig::Local, base_instructions: None, developer_instructions: None, + context_mode: ContextMode::Default, guardian_policy_config: None, include_permissions_instructions: true, include_apps_instructions: true, @@ -6624,6 +6666,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { experimental_thread_store: ThreadStoreConfig::Local, base_instructions: None, developer_instructions: None, + context_mode: ContextMode::Default, guardian_policy_config: None, include_permissions_instructions: true, include_apps_instructions: true, @@ -6780,6 +6823,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { experimental_thread_store: ThreadStoreConfig::Local, base_instructions: None, developer_instructions: None, + context_mode: ContextMode::Default, guardian_policy_config: None, include_permissions_instructions: true, include_apps_instructions: true, @@ -6921,6 +6965,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { experimental_thread_store: ThreadStoreConfig::Local, base_instructions: None, developer_instructions: None, + context_mode: ContextMode::Default, guardian_policy_config: None, include_permissions_instructions: true, include_apps_instructions: true, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 20b2a923f8..58dc0598a0 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -78,6 +78,7 @@ use codex_model_provider_info::built_in_model_providers; use codex_model_provider_info::merge_configured_model_providers; use codex_models_manager::ModelsManagerConfig; use codex_protocol::config_types::AltScreenMode; +use codex_protocol::config_types::ContextMode as ProtocolContextMode; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; @@ -451,6 +452,9 @@ pub struct Config { /// Developer instructions override injected as a separate message. pub developer_instructions: Option, + /// Controls whether Codex loads project/user context into the model prompt. + pub context_mode: ContextMode, + /// Guardian-specific policy config override from requirements.toml or config.toml. /// This is inserted into the fixed guardian prompt template under the /// `# Policy Configuration` section rather than replacing the whole @@ -812,6 +816,28 @@ pub struct Config { pub otel: codex_config::types::OtelConfig, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ContextMode { + #[default] + Default, + Vanilla, +} + +impl ContextMode { + pub fn includes_context_enrichment(self) -> bool { + self == ContextMode::Default + } +} + +impl From for ProtocolContextMode { + fn from(value: ContextMode) -> Self { + match value { + ContextMode::Default => ProtocolContextMode::Default, + ContextMode::Vanilla => ProtocolContextMode::Vanilla, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct MultiAgentV2Config { pub max_concurrent_threads_per_session: usize, @@ -1796,6 +1822,7 @@ pub struct ConfigOverrides { pub zsh_path: Option, pub base_instructions: Option, pub developer_instructions: Option, + pub context_mode: Option, pub personality: Option, pub compact_prompt: Option, pub include_apply_patch_tool: Option, @@ -2038,8 +2065,6 @@ impl Config { guardian_policy_config_source: _, } = config_layer_stack.requirements().clone(); - let user_instructions = AgentsMdManager::load_global_instructions(Some(&codex_home)) - .map(|loaded| loaded.contents); let mut startup_warnings = config_layer_stack .startup_warnings() .unwrap_or_default() @@ -2064,6 +2089,7 @@ impl Config { zsh_path: zsh_path_override, base_instructions, developer_instructions, + context_mode, personality, compact_prompt, include_apply_patch_tool: include_apply_patch_tool_override, @@ -2085,6 +2111,14 @@ impl Config { "`sandbox_mode` and `default_permissions` overrides cannot both be set", )); } + let context_mode = context_mode.unwrap_or_default(); + let includes_context_enrichment = context_mode.includes_context_enrichment(); + let user_instructions = includes_context_enrichment + .then(|| { + AgentsMdManager::load_global_instructions(Some(&codex_home)) + .map(|loaded| loaded.contents) + }) + .flatten(); if permission_profile.is_some() && default_permissions_override.is_some() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, @@ -2681,37 +2715,46 @@ impl Config { // Load base instructions override from a file if specified. If the // path is relative, resolve it against the effective cwd so the // behaviour matches other path-like config values. - let model_instructions_path = config_profile - .model_instructions_file - .as_ref() - .or(cfg.model_instructions_file.as_ref()); - let file_base_instructions = Self::try_read_non_empty_file( - fs, - model_instructions_path, - "model instructions file", - ) - .await?; - let base_instructions = base_instructions - .or(file_base_instructions) - .or(cfg.instructions.clone()); - let developer_instructions = developer_instructions.or(cfg.developer_instructions); + let base_instructions = if includes_context_enrichment { + let model_instructions_path = config_profile + .model_instructions_file + .as_ref() + .or(cfg.model_instructions_file.as_ref()); + let file_base_instructions = Self::try_read_non_empty_file( + fs, + model_instructions_path, + "model instructions file", + ) + .await?; + base_instructions + .or(file_base_instructions) + .or(cfg.instructions.clone()) + } else { + None + }; + let developer_instructions = includes_context_enrichment + .then(|| developer_instructions.or(cfg.developer_instructions)) + .flatten(); let include_permissions_instructions = config_profile .include_permissions_instructions .or(cfg.include_permissions_instructions) .unwrap_or(true); - let include_apps_instructions = config_profile - .include_apps_instructions - .or(cfg.include_apps_instructions) - .unwrap_or(true); - let include_skill_instructions = cfg - .skills - .as_ref() - .and_then(|skills| skills.include_instructions) - .unwrap_or(true); - let include_environment_context = config_profile - .include_environment_context - .or(cfg.include_environment_context) - .unwrap_or(true); + let include_apps_instructions = includes_context_enrichment + && config_profile + .include_apps_instructions + .or(cfg.include_apps_instructions) + .unwrap_or(true); + let include_skill_instructions = includes_context_enrichment + && cfg + .skills + .as_ref() + .and_then(|skills| skills.include_instructions) + .unwrap_or(true); + let include_environment_context = includes_context_enrichment + && config_profile + .include_environment_context + .or(cfg.include_environment_context) + .unwrap_or(true); let guardian_policy_config = guardian_policy_config_from_requirements(config_layer_stack.requirements_toml()) .or_else(|| { @@ -2721,14 +2764,18 @@ impl Config { auto_review.policy.as_deref(), )) }); - let personality = personality - .or(config_profile.personality) - .or(cfg.personality) - .or_else(|| { - features - .enabled(Feature::Personality) - .then_some(Personality::Pragmatic) - }); + let personality = includes_context_enrichment + .then(|| { + personality + .or(config_profile.personality) + .or(cfg.personality) + .or_else(|| { + features + .enabled(Feature::Personality) + .then_some(Personality::Pragmatic) + }) + }) + .flatten(); let experimental_compact_prompt_path = config_profile .experimental_compact_prompt_file @@ -2919,6 +2966,7 @@ impl Config { base_instructions, personality, developer_instructions, + context_mode, compact_prompt, commit_attribution, include_permissions_instructions, diff --git a/codex-rs/core/src/personality_migration_tests.rs b/codex-rs/core/src/personality_migration_tests.rs index 4aef53a5c4..1fc8d1fdf4 100644 --- a/codex-rs/core/src/personality_migration_tests.rs +++ b/codex-rs/core/src/personality_migration_tests.rs @@ -54,6 +54,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R agent_nickname: None, agent_role: None, model_provider: None, + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index c18976fde1..ffc83c19fa 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -474,9 +474,13 @@ impl Codex { let fs = environment_selections.primary_filesystem(); let plugins_input = config.plugins_config_input(); let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; - let effective_skill_roots = plugin_outcome.effective_skill_roots(); - let skills_input = skills_load_input_from_config(&config, effective_skill_roots); - let loaded_skills = skills_manager.skills_for_config(&skills_input, fs).await; + let loaded_skills = if config.context_mode.includes_context_enrichment() { + let effective_skill_roots = plugin_outcome.effective_skill_roots(); + let skills_input = skills_load_input_from_config(&config, effective_skill_roots); + skills_manager.skills_for_config(&skills_input, fs).await + } else { + SkillLoadOutcome::default() + }; for err in &loaded_skills.errors { error!( @@ -495,9 +499,13 @@ impl Codex { } let primary_environment = environment_selections.primary_environment(); - let user_instructions = AgentsMdManager::new(&config) - .user_instructions(primary_environment.as_deref()) - .await; + let user_instructions = if config.context_mode.includes_context_enrichment() { + AgentsMdManager::new(&config) + .user_instructions(primary_environment.as_deref()) + .await + } else { + None + }; let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) { // Guardian review should rely on the built-in shell safety checks, @@ -598,6 +606,7 @@ impl Codex { developer_instructions: config.developer_instructions.clone(), user_instructions, personality: config.personality, + context_mode: config.context_mode, base_instructions, compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), @@ -2578,16 +2587,22 @@ impl Session { } let separate_guardian_developer_message = crate::guardian::is_guardian_reviewer_source(&session_source); + let includes_context_enrichment = turn_context + .config + .context_mode + .includes_context_enrichment(); // Keep the guardian policy prompt out of the aggregated developer bundle so it // stays isolated as its own top-level developer message for guardian subagents. - if !separate_guardian_developer_message + if includes_context_enrichment + && !separate_guardian_developer_message && let Some(developer_instructions) = turn_context.developer_instructions.as_deref() && !developer_instructions.is_empty() { developer_sections.push(developer_instructions.to_string()); } // Add developer instructions for memories. - if turn_context.features.enabled(Feature::MemoryTool) + if includes_context_enrichment + && turn_context.features.enabled(Feature::MemoryTool) && turn_context.config.memories.use_memories && let Some(memory_prompt) = build_memory_tool_developer_instructions(&turn_context.config.codex_home).await @@ -2595,8 +2610,9 @@ impl Session { developer_sections.push(memory_prompt); } // Add developer instructions from collaboration_mode if they exist and are non-empty - if let Some(collab_instructions) = - CollaborationModeInstructions::from_collaboration_mode(&collaboration_mode) + if includes_context_enrichment + && let Some(collab_instructions) = + CollaborationModeInstructions::from_collaboration_mode(&collaboration_mode) { developer_sections.push(collab_instructions.render()); } @@ -2607,7 +2623,8 @@ impl Session { ) { developer_sections.push(realtime_update); } - if self.features.enabled(Feature::Personality) + if includes_context_enrichment + && self.features.enabled(Feature::Personality) && let Some(personality) = turn_context.personality { let model_info = turn_context.model_info.clone(); @@ -2624,7 +2641,10 @@ impl Session { .push(PersonalitySpecInstructions::new(personality_message).render()); } } - if turn_context.config.include_apps_instructions && turn_context.apps_enabled() { + if includes_context_enrichment + && turn_context.config.include_apps_instructions + && turn_context.apps_enabled() + { let mcp_connection_manager = self.services.mcp_connection_manager.read().await; let accessible_and_enabled_connectors = connectors::list_accessible_and_enabled_connectors_from_manager( @@ -2638,7 +2658,7 @@ impl Session { developer_sections.push(apps_instructions.render()); } } - if turn_context.config.include_skill_instructions { + if includes_context_enrichment && turn_context.config.include_skill_instructions { let available_skills = build_available_skills( &turn_context.turn_skills.outcome, default_skill_metadata_budget(turn_context.model_info.context_window), @@ -2661,24 +2681,29 @@ impl Session { developer_sections.push(skills_instructions.render()); } } - let loaded_plugins = self - .services - .plugins_manager - .plugins_for_config(&turn_context.config.plugins_config_input()) - .await; - if let Some(plugin_instructions) = - AvailablePluginsInstructions::from_plugins(loaded_plugins.capability_summaries()) - { - developer_sections.push(plugin_instructions.render()); + if includes_context_enrichment { + let loaded_plugins = self + .services + .plugins_manager + .plugins_for_config(&turn_context.config.plugins_config_input()) + .await; + if let Some(plugin_instructions) = + AvailablePluginsInstructions::from_plugins(loaded_plugins.capability_summaries()) + { + developer_sections.push(plugin_instructions.render()); + } } - if turn_context.features.enabled(Feature::CodexGitCommit) + if includes_context_enrichment + && turn_context.features.enabled(Feature::CodexGitCommit) && let Some(commit_message_instruction) = commit_message_trailer_instruction( turn_context.config.commit_attribution.as_deref(), ) { developer_sections.push(commit_message_instruction); } - if let Some(user_instructions) = turn_context.user_instructions.as_deref() { + if includes_context_enrichment + && let Some(user_instructions) = turn_context.user_instructions.as_deref() + { contextual_user_sections.push( UserInstructions { text: user_instructions.to_string(), @@ -2687,7 +2712,7 @@ impl Session { .render(), ); } - if turn_context.config.include_environment_context { + if includes_context_enrichment && turn_context.config.include_environment_context { let subagents = self .services .agent_control diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 50b3345d61..40dd4bb90d 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1,6 +1,7 @@ use super::*; use crate::goals::GoalRuntimeState; use codex_otel::LEGACY_NOTIFY_CONFIGURED_METRIC; +use codex_protocol::config_types::ContextMode as ProtocolContextMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::protocol::TurnEnvironmentSelection; @@ -51,6 +52,9 @@ pub(crate) struct SessionConfiguration { /// Personality preference for the model. pub(super) personality: Option, + /// Controls whether Codex loads project/user context into the model prompt. + pub(super) context_mode: crate::config::ContextMode, + /// Base instructions for the session. pub(super) base_instructions: String, @@ -141,6 +145,7 @@ impl SessionConfiguration { ephemeral: self.original_config_do_not_use.ephemeral, reasoning_effort: self.collaboration_mode.reasoning_effort(), personality: self.personality, + context_mode: self.context_mode, session_source: self.session_source.clone(), } } @@ -400,6 +405,9 @@ impl Session { metadata: ThreadPersistenceMetadata { cwd: Some(config.cwd.to_path_buf()), model_provider: config.model_provider_id.clone(), + context_mode: ProtocolContextMode::from( + session_configuration.context_mode, + ), memory_mode: if config.memories.generate_memories { ThreadMemoryMode::Enabled } else { @@ -422,6 +430,9 @@ impl Session { metadata: ThreadPersistenceMetadata { cwd: Some(config.cwd.to_path_buf()), model_provider: config.model_provider_id.clone(), + context_mode: ProtocolContextMode::from( + session_configuration.context_mode, + ), memory_mode: if config.memories.generate_memories { ThreadMemoryMode::Enabled } else { diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 89633daf35..83dc96f89e 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -2354,6 +2354,7 @@ async fn set_rate_limits_retains_previous_credits() { user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, + context_mode: config.context_mode, base_instructions: config .base_instructions .clone() @@ -2457,6 +2458,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, + context_mode: config.context_mode, base_instructions: config .base_instructions .clone() @@ -2733,6 +2735,7 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf { metadata: ThreadPersistenceMetadata { cwd: Some(config.cwd.to_path_buf()), model_provider: config.model_provider_id.clone(), + context_mode: codex_protocol::config_types::ContextMode::Default, memory_mode: if config.memories.generate_memories { ThreadMemoryMode::Enabled } else { @@ -2915,6 +2918,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, + context_mode: config.context_mode, base_instructions: config .base_instructions .clone() @@ -3438,6 +3442,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, + context_mode: config.context_mode, base_instructions: config .base_instructions .clone() @@ -3543,6 +3548,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, + context_mode: config.context_mode, base_instructions: config .base_instructions .clone() @@ -3755,6 +3761,7 @@ async fn make_session_with_config_and_rx( user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, + context_mode: config.context_mode, base_instructions: config .base_instructions .clone() @@ -4726,6 +4733,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() { metadata: ThreadPersistenceMetadata { cwd: Some(config.cwd.to_path_buf()), model_provider: config.model_provider_id.clone(), + context_mode: codex_protocol::config_types::ContextMode::Default, memory_mode: if config.memories.generate_memories { ThreadMemoryMode::Enabled } else { @@ -5025,6 +5033,7 @@ where user_instructions: config.user_instructions.clone(), service_tier: None, personality: config.personality, + context_mode: config.context_mode, base_instructions: config .base_instructions .clone() @@ -5808,6 +5817,45 @@ async fn build_initial_context_trims_skill_metadata_from_context_window_budget() ); } +#[tokio::test] +async fn build_initial_context_omits_context_enrichment_in_vanilla_mode() { + let (session, mut turn_context) = make_session_and_context().await; + let mut config = (*turn_context.config).clone(); + config.context_mode = crate::config::ContextMode::Vanilla; + config.include_apps_instructions = true; + config.include_skill_instructions = true; + config.include_environment_context = true; + turn_context.config = Arc::new(config); + turn_context.developer_instructions = Some("VANILLA_DEVELOPER_INSTRUCTIONS_MARKER".to_string()); + turn_context.user_instructions = Some("VANILLA_USER_INSTRUCTIONS_MARKER".to_string()); + turn_context.personality = Some(codex_protocol::config_types::Personality::Pragmatic); + let mut outcome = SkillLoadOutcome::default(); + outcome.skills = vec![SkillMetadata { + name: "vanilla-repo-skill".to_string(), + description: "VANILLA_SKILL_DESCRIPTION_MARKER".to_string(), + short_description: None, + interface: None, + dependencies: None, + policy: None, + path_to_skills_md: test_path_buf("/tmp/vanilla-repo-skill/SKILL.md").abs(), + scope: SkillScope::Repo, + }]; + turn_context.turn_skills = TurnSkillsContext::new(Arc::new(outcome)); + + let initial_context = session.build_initial_context(&turn_context).await; + let visible_text = developer_input_texts(&initial_context) + .into_iter() + .chain(user_input_texts(&initial_context)) + .collect::>() + .join("\n"); + + assert!(!visible_text.contains("VANILLA_DEVELOPER_INSTRUCTIONS_MARKER")); + assert!(!visible_text.contains("VANILLA_USER_INSTRUCTIONS_MARKER")); + assert!(!visible_text.contains("vanilla-repo-skill")); + assert!(!visible_text.contains("VANILLA_SKILL_DESCRIPTION_MARKER")); + assert!(!visible_text.contains("")); +} + #[test] fn emit_thread_start_skill_metrics_records_enabled_kept_and_truncated_values() { let session_telemetry = test_session_telemetry_without_metadata(); diff --git a/codex-rs/core/tests/suite/personality_migration.rs b/codex-rs/core/tests/suite/personality_migration.rs index f300745129..0ef34d79fd 100644 --- a/codex-rs/core/tests/suite/personality_migration.rs +++ b/codex-rs/core/tests/suite/personality_migration.rs @@ -70,6 +70,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R agent_nickname: None, agent_role: None, model_provider: None, + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, @@ -115,6 +116,7 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re agent_nickname: None, agent_role: None, model_provider: None, + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, diff --git a/codex-rs/core/tests/suite/rollout_list_find.rs b/codex-rs/core/tests/suite/rollout_list_find.rs index eef0d0f5f4..2de238ad5b 100644 --- a/codex-rs/core/tests/suite/rollout_list_find.rs +++ b/codex-rs/core/tests/suite/rollout_list_find.rs @@ -174,6 +174,7 @@ async fn find_locates_rollout_file_written_by_recorder() -> std::io::Result<()> /*forked_from_id*/ None, SessionSource::Exec, BaseInstructions::default(), + codex_protocol::config_types::ContextMode::Default, Vec::new(), EventPersistenceMode::Limited, ), diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index 8250f5493d..387ee8042f 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -148,6 +148,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> { agent_nickname: None, agent_role: None, model_provider: None, + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: Some(dynamic_tools_for_hook), memory_mode: None, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index f2f0ed030b..9e5aabc72d 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -412,6 +412,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result zsh_path: None, base_instructions: None, developer_instructions: None, + context_mode: None, personality: None, compact_prompt: None, include_apply_patch_tool: None, diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index da83ee858a..8dcac03c97 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -77,6 +77,20 @@ pub enum SandboxMode { DangerFullAccess, } +#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub enum ContextMode { + #[default] + Default, + Vanilla, +} + +impl ContextMode { + pub fn is_default(&self) -> bool { + *self == ContextMode::Default + } +} + #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Display, TS)] #[strum(serialize_all = "snake_case")] #[ts(type = r#""user" | "auto_review" | "guardian_subagent""#)] diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 60137fa8b0..3ad8533f77 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -18,6 +18,7 @@ use crate::ThreadId; use crate::approvals::ElicitationRequestEvent; use crate::config_types::ApprovalsReviewer; use crate::config_types::CollaborationMode; +use crate::config_types::ContextMode; use crate::config_types::ModeKind; use crate::config_types::Personality; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; @@ -2526,6 +2527,22 @@ impl InitialHistory { }), } } + + pub fn get_context_mode(&self) -> Option { + match self { + InitialHistory::New | InitialHistory::Cleared => None, + InitialHistory::Resumed(resumed) => { + resumed.history.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.context_mode), + _ => None, + }) + } + InitialHistory::Forked(items) => items.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.context_mode), + _ => None, + }), + } + } } fn session_cwd_from_items(items: &[RolloutItem]) -> Option { @@ -2731,6 +2748,8 @@ pub struct SessionMeta { #[serde(skip_serializing_if = "Option::is_none")] pub agent_path: Option, pub model_provider: Option, + #[serde(default, skip_serializing_if = "ContextMode::is_default")] + pub context_mode: ContextMode, /// base_instructions for the session. This *should* always be present when creating a new session, /// but may be missing for older sessions. If not present, fall back to rendering the base_instructions /// from ModelsManager. @@ -2755,6 +2774,7 @@ impl Default for SessionMeta { agent_role: None, agent_path: None, model_provider: None, + context_mode: ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, diff --git a/codex-rs/rollout/src/metadata_tests.rs b/codex-rs/rollout/src/metadata_tests.rs index c94cd0be7e..b11f015995 100644 --- a/codex-rs/rollout/src/metadata_tests.rs +++ b/codex-rs/rollout/src/metadata_tests.rs @@ -44,6 +44,7 @@ async fn extract_metadata_from_rollout_uses_session_meta() { agent_nickname: None, agent_role: None, model_provider: Some("openai".to_string()), + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, @@ -95,6 +96,7 @@ async fn extract_metadata_from_rollout_returns_latest_memory_mode() { agent_nickname: None, agent_role: None, model_provider: Some("openai".to_string()), + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, @@ -354,6 +356,7 @@ fn write_rollout_in_sessions_with_cwd( agent_nickname: None, agent_role: None, model_provider: Some("test-provider".to_string()), + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, diff --git a/codex-rs/rollout/src/recorder.rs b/codex-rs/rollout/src/recorder.rs index dc2f08b7ab..ca0f31605b 100644 --- a/codex-rs/rollout/src/recorder.rs +++ b/codex-rs/rollout/src/recorder.rs @@ -12,6 +12,7 @@ use std::sync::Mutex; use chrono::SecondsFormat; use chrono::Utc; use codex_protocol::ThreadId; +use codex_protocol::config_types::ContextMode; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::BaseInstructions; use codex_utils_string::truncate_middle_chars; @@ -90,6 +91,7 @@ pub enum RolloutRecorderParams { forked_from_id: Option, source: SessionSource, base_instructions: BaseInstructions, + context_mode: ContextMode, dynamic_tools: Vec, event_persistence_mode: EventPersistenceMode, }, @@ -166,6 +168,7 @@ impl RolloutRecorderParams { forked_from_id: Option, source: SessionSource, base_instructions: BaseInstructions, + context_mode: ContextMode, dynamic_tools: Vec, event_persistence_mode: EventPersistenceMode, ) -> Self { @@ -174,6 +177,7 @@ impl RolloutRecorderParams { forked_from_id, source, base_instructions, + context_mode, dynamic_tools, event_persistence_mode, } @@ -661,6 +665,7 @@ impl RolloutRecorder { forked_from_id, source, base_instructions, + context_mode, dynamic_tools, event_persistence_mode, } => { @@ -689,6 +694,7 @@ impl RolloutRecorder { agent_path: source.get_agent_path().map(Into::into), source, model_provider: Some(config.model_provider_id().to_string()), + context_mode, base_instructions: Some(base_instructions), dynamic_tools: if dynamic_tools.is_empty() { None diff --git a/codex-rs/rollout/src/recorder_tests.rs b/codex-rs/rollout/src/recorder_tests.rs index 0138db72df..9c9ac92a9a 100644 --- a/codex-rs/rollout/src/recorder_tests.rs +++ b/codex-rs/rollout/src/recorder_tests.rs @@ -232,6 +232,7 @@ async fn recorder_materializes_on_flush_with_pending_items() -> std::io::Result< /*forked_from_id*/ None, SessionSource::Exec, BaseInstructions::default(), + codex_protocol::config_types::ContextMode::Default, Vec::new(), EventPersistenceMode::Limited, ), @@ -312,6 +313,7 @@ async fn persist_reports_filesystem_error_and_retries_buffered_items() -> std::i /*forked_from_id*/ None, SessionSource::Exec, BaseInstructions::default(), + codex_protocol::config_types::ContextMode::Default, Vec::new(), EventPersistenceMode::Limited, ), @@ -411,6 +413,7 @@ async fn metadata_irrelevant_events_touch_state_db_updated_at() -> std::io::Resu /*forked_from_id*/ None, SessionSource::Cli, BaseInstructions::default(), + codex_protocol::config_types::ContextMode::Default, Vec::new(), EventPersistenceMode::Limited, ), diff --git a/codex-rs/rollout/src/session_index_tests.rs b/codex-rs/rollout/src/session_index_tests.rs index c6a539fb28..f00eed70b4 100644 --- a/codex-rs/rollout/src/session_index_tests.rs +++ b/codex-rs/rollout/src/session_index_tests.rs @@ -36,6 +36,7 @@ fn write_rollout_with_metadata(path: &Path, thread_id: ThreadId) -> std::io::Res agent_nickname: None, agent_role: None, model_provider: Some("test-provider".into()), + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, diff --git a/codex-rs/rollout/src/tests.rs b/codex-rs/rollout/src/tests.rs index fba8a9827a..adc5ece4ef 100644 --- a/codex-rs/rollout/src/tests.rs +++ b/codex-rs/rollout/src/tests.rs @@ -1149,6 +1149,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { agent_nickname: None, agent_role: None, model_provider: Some("test-provider".into()), + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index a4a0ab0f6a..34cdb0cce5 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -282,6 +282,7 @@ mod tests { agent_nickname: None, agent_role: None, model_provider: Some("openai".to_string()), + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, @@ -415,6 +416,7 @@ mod tests { agent_nickname: None, agent_role: None, model_provider: Some("openai".to_string()), + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, diff --git a/codex-rs/state/src/runtime/threads.rs b/codex-rs/state/src/runtime/threads.rs index 906a3bb39a..ba8f7a35e7 100644 --- a/codex-rs/state/src/runtime/threads.rs +++ b/codex-rs/state/src/runtime/threads.rs @@ -1362,6 +1362,7 @@ mod tests { agent_nickname: None, agent_role: None, model_provider: None, + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: Some("polluted".to_string()), @@ -1420,6 +1421,7 @@ mod tests { agent_nickname: None, agent_role: None, model_provider: None, + context_mode: codex_protocol::config_types::ContextMode::Default, base_instructions: None, dynamic_tools: None, memory_mode: None, diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index cc2262512d..9d0fcf75f3 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -159,6 +159,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R model_auto_compact_token_limit: None, model_provider_id, model_provider, + context_mode: Default::default(), personality: None, permissions: Permissions { approval_policy: Constrained::allow_any(AskForApproval::Never), diff --git a/codex-rs/thread-store/src/local/create_thread.rs b/codex-rs/thread-store/src/local/create_thread.rs index e444e5c91d..013f4351e1 100644 --- a/codex-rs/thread-store/src/local/create_thread.rs +++ b/codex-rs/thread-store/src/local/create_thread.rs @@ -35,6 +35,7 @@ pub(super) async fn create_thread( params.forked_from_id, params.source, params.base_instructions, + params.metadata.context_mode, params.dynamic_tools, event_persistence_mode(params.event_persistence_mode), ), diff --git a/codex-rs/thread-store/src/local/mod.rs b/codex-rs/thread-store/src/local/mod.rs index 04dd8b2490..e4bf2920c7 100644 --- a/codex-rs/thread-store/src/local/mod.rs +++ b/codex-rs/thread-store/src/local/mod.rs @@ -216,7 +216,7 @@ impl ThreadStore for LocalThreadStore { return read_thread::read_thread_by_rollout_path( self, rollout_path, - /*include_archived*/ true, + params.include_archived, /*include_history*/ true, ) .await? @@ -533,6 +533,7 @@ mod tests { metadata: ThreadPersistenceMetadata { cwd: None, model_provider: "test-provider".to_string(), + context_mode: codex_protocol::config_types::ContextMode::Default, memory_mode: ThreadMemoryMode::Enabled, }, event_persistence_mode: ThreadEventPersistenceMode::Limited, @@ -762,6 +763,7 @@ mod tests { ThreadPersistenceMetadata { cwd: Some(std::env::current_dir().expect("cwd")), model_provider: "test-provider".to_string(), + context_mode: codex_protocol::config_types::ContextMode::Default, memory_mode: ThreadMemoryMode::Enabled, } } diff --git a/codex-rs/thread-store/src/local/read_thread.rs b/codex-rs/thread-store/src/local/read_thread.rs index 8b3d3160db..f99ba3bb05 100644 --- a/codex-rs/thread-store/src/local/read_thread.rs +++ b/codex-rs/thread-store/src/local/read_thread.rs @@ -71,6 +71,11 @@ pub(super) async fn read_thread( })?; let mut thread = read_thread_from_rollout_path(store, path).await?; + if !params.include_archived && thread.archived_at.is_some() { + return Err(ThreadStoreError::InvalidRequest { + message: format!("thread {thread_id} is archived"), + }); + } attach_history_if_requested(&mut thread, params.include_history).await?; Ok(thread) } diff --git a/codex-rs/thread-store/src/local/update_thread_metadata.rs b/codex-rs/thread-store/src/local/update_thread_metadata.rs index fba0172525..5c91279677 100644 --- a/codex-rs/thread-store/src/local/update_thread_metadata.rs +++ b/codex-rs/thread-store/src/local/update_thread_metadata.rs @@ -523,6 +523,7 @@ mod tests { ThreadPersistenceMetadata { cwd: Some(std::env::current_dir().expect("cwd")), model_provider: "test-provider".to_string(), + context_mode: codex_protocol::config_types::ContextMode::Default, memory_mode: ThreadMemoryMode::Enabled, } } diff --git a/codex-rs/thread-store/src/remote/mod.rs b/codex-rs/thread-store/src/remote/mod.rs index 3e74a45f4b..78a0d602e9 100644 --- a/codex-rs/thread-store/src/remote/mod.rs +++ b/codex-rs/thread-store/src/remote/mod.rs @@ -350,6 +350,7 @@ mod tests { let metadata = ThreadPersistenceMetadata { cwd: Some(PathBuf::from("/workspace")), model_provider: "test-provider".to_string(), + context_mode: codex_protocol::config_types::ContextMode::Default, memory_mode: ThreadMemoryMode::Enabled, }; @@ -382,6 +383,7 @@ mod tests { let metadata = ThreadPersistenceMetadata { cwd: Some(PathBuf::from("/workspace")), model_provider: "test-provider".to_string(), + context_mode: codex_protocol::config_types::ContextMode::Default, memory_mode: ThreadMemoryMode::Disabled, }; diff --git a/codex-rs/thread-store/src/types.rs b/codex-rs/thread-store/src/types.rs index 85bde023bd..c5b75f0565 100644 --- a/codex-rs/thread-store/src/types.rs +++ b/codex-rs/thread-store/src/types.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use chrono::DateTime; use chrono::Utc; use codex_protocol::ThreadId; +use codex_protocol::config_types::ContextMode; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::BaseInstructions; use codex_protocol::openai_models::ReasoningEffort; @@ -35,6 +36,8 @@ pub struct ThreadPersistenceMetadata { pub cwd: Option, /// Model provider associated with the thread. pub model_provider: String, + /// Context mode associated with the thread. + pub context_mode: ContextMode, /// Memory mode associated with the live thread. pub memory_mode: MemoryMode, }