From df5f12285454ce0a6e78a1ce084b57b6860add41 Mon Sep 17 00:00:00 2001 From: rka-oai Date: Thu, 18 Jun 2026 11:39:02 -0700 Subject: [PATCH] Add Config for Time Reminders (varlatency 1/n) (#28822) ## Summary Example: > [features.current_time_reminder] enabled = true reminder_interval_model_requests = 1 clock_source = "system" ## Testing - `just test -p codex-core varlatency` - `just test -p codex-core lock_contains_prompts_and_materializes_features` - `just fix -p codex-core -p codex-config -p codex-features` --- codex-rs/config/src/schema.rs | 9 ++++ codex-rs/core/config.schema.json | 40 ++++++++++++++ codex-rs/core/src/config/config_tests.rs | 61 ++++++++++++++++++++++ codex-rs/core/src/config/mod.rs | 58 ++++++++++++++++++++ codex-rs/core/src/session/config_lock.rs | 20 +++++++ codex-rs/features/src/feature_configs.rs | 30 +++++++++++ codex-rs/features/src/lib.rs | 22 ++++++++ codex-rs/thread-manager-sample/src/main.rs | 1 + 8 files changed, 241 insertions(+) diff --git a/codex-rs/config/src/schema.rs b/codex-rs/config/src/schema.rs index 6d3f901bf..fad074e9c 100644 --- a/codex-rs/config/src/schema.rs +++ b/codex-rs/config/src/schema.rs @@ -53,6 +53,15 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema { ); continue; } + if feature.id == codex_features::Feature::CurrentTimeReminder { + validation.properties.insert( + feature.key.to_string(), + schema_gen.subschema_for::>(), + ); + continue; + } if feature.id == codex_features::Feature::AppsMcpPathOverride { validation.properties.insert( feature.key.to_string(), diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 852f6de39..7faf569f7 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -476,6 +476,9 @@ "connectors": { "type": "boolean" }, + "current_time_reminder": { + "$ref": "#/definitions/FeatureToml_for_CurrentTimeReminderConfigToml" + }, "default_mode_request_user_input": { "type": "boolean" }, @@ -799,6 +802,30 @@ }, "type": "object" }, + "CurrentTimeReminderConfigToml": { + "additionalProperties": false, + "properties": { + "clock_source": { + "$ref": "#/definitions/CurrentTimeSource" + }, + "enabled": { + "type": "boolean" + }, + "reminder_interval_model_requests": { + "format": "uint64", + "minimum": 1.0, + "type": "integer" + } + }, + "type": "object" + }, + "CurrentTimeSource": { + "enum": [ + "system", + "external" + ], + "type": "string" + }, "DebugConfigLockToml": { "additionalProperties": false, "properties": { @@ -891,6 +918,16 @@ } ] }, + "FeatureToml_for_CurrentTimeReminderConfigToml": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/CurrentTimeReminderConfigToml" + } + ] + }, "FeatureToml_for_MultiAgentV2ConfigToml": { "anyOf": [ { @@ -4679,6 +4716,9 @@ "connectors": { "type": "boolean" }, + "current_time_reminder": { + "$ref": "#/definitions/FeatureToml_for_CurrentTimeReminderConfigToml" + }, "default_mode_request_user_input": { "type": "boolean" }, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 2244e3e1b..b13c9575f 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -522,6 +522,67 @@ async fn load_config_rejects_enabled_rollout_budget_without_limit() -> std::io:: Ok(()) } +#[tokio::test] +async fn load_config_resolves_current_time_reminder() -> std::io::Result<()> { + for (config_toml, expected) in [ + ( + r#" +[features] +current_time_reminder = true +"#, + CurrentTimeReminderConfig::default(), + ), + ( + r#" +[features.current_time_reminder] +enabled = true +reminder_interval_model_requests = 4 +clock_source = "external" +"#, + CurrentTimeReminderConfig { + reminder_interval_model_requests: 4, + clock_source: CurrentTimeSource::External, + }, + ), + ] { + let config = load_current_time_reminder_config(config_toml).await?; + assert!(config.features.enabled(Feature::CurrentTimeReminder)); + assert_eq!(config.current_time_reminder, Some(expected)); + } + Ok(()) +} + +#[tokio::test] +async fn load_config_rejects_zero_current_time_reminder_interval() -> std::io::Result<()> { + let error = load_current_time_reminder_config( + r#" +[features.current_time_reminder] +enabled = true +reminder_interval_model_requests = 0 +"#, + ) + .await + .expect_err("zero reminder interval should be rejected"); + + assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!( + error.to_string(), + "features.current_time_reminder.reminder_interval_model_requests must be positive" + ); + Ok(()) +} + +async fn load_current_time_reminder_config(config_toml: &str) -> std::io::Result { + let codex_home = tempdir()?; + let config_toml = toml::from_str(config_toml).expect("TOML should deserialize"); + Config::load_from_base_config_with_overrides( + config_toml, + ConfigOverrides::default(), + codex_home.abs(), + ) + .await +} + #[test] fn rejects_provider_auth_with_env_key() { let err = toml::from_str::( diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 042761336..a1854067f 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -58,6 +58,8 @@ use codex_core_plugins::PluginsConfigInput; use codex_exec_server::ExecutorFileSystem; use codex_exec_server::LOCAL_FS; use codex_features::CodeModeConfigToml; +use codex_features::CurrentTimeReminderConfigToml; +use codex_features::CurrentTimeSource; use codex_features::Feature; use codex_features::FeatureConfigSource; use codex_features::FeatureOverrides; @@ -1022,6 +1024,8 @@ pub struct Config { /// Shared token budget for the root thread and its sub-agents. pub rollout_budget: Option, + /// Current-time reminder configuration, when enabled. + pub current_time_reminder: Option, /// Centralized feature flags; source of truth for feature gating. pub features: ManagedFeatures, @@ -1075,6 +1079,21 @@ pub struct RolloutBudgetConfig { pub prefill_token_weight: f64, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub struct CurrentTimeReminderConfig { + pub reminder_interval_model_requests: u64, + pub clock_source: CurrentTimeSource, +} + +impl Default for CurrentTimeReminderConfig { + fn default() -> Self { + Self { + reminder_interval_model_requests: 1, + clock_source: CurrentTimeSource::System, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct MultiAgentV2Config { pub max_concurrent_threads_per_session: usize, @@ -2539,6 +2558,34 @@ fn resolve_rollout_budget_config( })) } +fn resolve_current_time_reminder_config( + config_toml: &ConfigToml, + features: &ManagedFeatures, +) -> std::io::Result> { + if !features.enabled(Feature::CurrentTimeReminder) { + return Ok(None); + } + + let base = current_time_reminder_toml_config(config_toml.features.as_ref()); + let default = CurrentTimeReminderConfig::default(); + let reminder_interval_model_requests = base + .and_then(|config| config.reminder_interval_model_requests) + .unwrap_or(default.reminder_interval_model_requests); + if reminder_interval_model_requests == 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "features.current_time_reminder.reminder_interval_model_requests must be positive", + )); + } + + Ok(Some(CurrentTimeReminderConfig { + reminder_interval_model_requests, + clock_source: base + .and_then(|config| config.clock_source) + .unwrap_or(default.clock_source), + })) +} + fn resolve_terminal_resize_reflow_config(config_toml: &ConfigToml) -> TerminalResizeReflowConfig { let Some(tui) = config_toml.tui.as_ref() else { return TerminalResizeReflowConfig::default(); @@ -2578,6 +2625,15 @@ fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiA } } +fn current_time_reminder_toml_config( + features: Option<&FeaturesToml>, +) -> Option<&CurrentTimeReminderConfigToml> { + match features?.current_time_reminder.as_ref()? { + FeatureToml::Enabled(_) => None, + FeatureToml::Config(config) => Some(config), + } +} + fn network_proxy_toml_config(features: Option<&FeaturesToml>) -> Option<&NetworkProxyConfigToml> { match features?.network_proxy.as_ref()? { FeatureToml::Enabled(_) => None, @@ -3217,6 +3273,7 @@ impl Config { let code_mode = resolve_code_mode_config(&cfg); let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg); let rollout_budget = resolve_rollout_budget_config(&cfg, &features)?; + let current_time_reminder = resolve_current_time_reminder_config(&cfg, &features)?; let terminal_resize_reflow = resolve_terminal_resize_reflow_config(&cfg); let agent_roles = @@ -3758,6 +3815,7 @@ impl Config { ghost_snapshot, multi_agent_v2, rollout_budget, + current_time_reminder, features, suppress_unstable_features_warning: cfg .suppress_unstable_features_warning diff --git a/codex-rs/core/src/session/config_lock.rs b/codex-rs/core/src/session/config_lock.rs index b5f1689ad..61c39a5dd 100644 --- a/codex-rs/core/src/session/config_lock.rs +++ b/codex-rs/core/src/session/config_lock.rs @@ -2,6 +2,7 @@ use anyhow::Context; use codex_config::config_toml::ConfigLockfileToml; use codex_config::config_toml::ConfigToml; use codex_config::types::MemoriesToml; +use codex_features::CurrentTimeReminderConfigToml; use codex_features::Feature; use codex_features::FeatureToml; use codex_features::FeaturesToml; @@ -155,6 +156,12 @@ fn save_config_resolved_fields( rollout_budget.enabled = Some(config.features.enabled(Feature::RolloutBudget)); features.rollout_budget = Some(FeatureToml::Config(rollout_budget)); } + if let Some(current_time_reminder) = config.current_time_reminder.as_ref() { + let mut current_time_reminder: CurrentTimeReminderConfigToml = + resolved_config_to_toml(current_time_reminder, "features.current_time_reminder")?; + current_time_reminder.enabled = Some(config.features.enabled(Feature::CurrentTimeReminder)); + features.current_time_reminder = Some(FeatureToml::Config(current_time_reminder)); + } lock_config.memories = Some(resolved_config_to_toml::( &config.memories, "memories", @@ -227,6 +234,11 @@ mod tests { .features .enable(Feature::RolloutBudget) .expect("rollout_budget should be enableable in tests"); + config.current_time_reminder = Some(crate::config::CurrentTimeReminderConfig::default()); + config + .features + .enable(Feature::CurrentTimeReminder) + .expect("current_time_reminder should be enableable in tests"); sc.original_config_do_not_use = Arc::new(config); sc.base_instructions = "resolved instructions".to_string(); sc.developer_instructions = Some("resolved developer instructions".to_string()); @@ -302,6 +314,14 @@ mod tests { prefill_token_weight: Some(0.25), })) ); + assert_eq!( + features.current_time_reminder, + Some(FeatureToml::Config(CurrentTimeReminderConfigToml { + enabled: Some(true), + reminder_interval_model_requests: Some(1), + clock_source: Some(codex_features::CurrentTimeSource::System), + })) + ); assert_eq!(lockfile.version, crate::config_lock::CONFIG_LOCK_VERSION); } diff --git a/codex-rs/features/src/feature_configs.rs b/codex-rs/features/src/feature_configs.rs index ad4b6da63..dc78d8cd4 100644 --- a/codex-rs/features/src/feature_configs.rs +++ b/codex-rs/features/src/feature_configs.rs @@ -102,6 +102,36 @@ impl FeatureConfig for RolloutBudgetConfigToml { } } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum CurrentTimeSource { + #[default] + System, + External, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CurrentTimeReminderConfigToml { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(range(min = 1))] + pub reminder_interval_model_requests: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub clock_source: Option, +} + +impl FeatureConfig for CurrentTimeReminderConfigToml { + fn enabled(&self) -> Option { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = Some(enabled); + } +} + #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub(crate) struct RemovedAppsMcpPathOverrideConfigToml { diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 731ea20a1..58f291407 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -17,6 +17,8 @@ use toml::Table; mod feature_configs; mod legacy; pub use feature_configs::CodeModeConfigToml; +pub use feature_configs::CurrentTimeReminderConfigToml; +pub use feature_configs::CurrentTimeSource; pub use feature_configs::MultiAgentV2ConfigToml; pub use feature_configs::NetworkProxyConfigToml; pub use feature_configs::NetworkProxyDomainPermissionToml; @@ -206,6 +208,8 @@ pub enum Feature { TokenBudget, /// Track and report a shared token budget across a session's agent threads. RolloutBudget, + /// Add current-time reminders to model-visible context. + CurrentTimeReminder, /// Expose an input-interruptible sleep tool. SleepTool, /// Route MCP tool approval prompts through the MCP elicitation request path. @@ -623,6 +627,8 @@ pub struct FeaturesToml { pub multi_agent_v2: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub rollout_budget: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_time_reminder: Option>, #[serde(default, rename = "apps_mcp_path_override", skip_serializing)] #[schemars(skip)] removed_apps_mcp_path_override: Option>, @@ -658,6 +664,13 @@ impl FeaturesToml { if let Some(enabled) = self.rollout_budget.as_ref().and_then(FeatureToml::enabled) { entries.insert(Feature::RolloutBudget.key().to_string(), enabled); } + if let Some(enabled) = self + .current_time_reminder + .as_ref() + .and_then(FeatureToml::enabled) + { + entries.insert(Feature::CurrentTimeReminder.key().to_string(), enabled); + } if let Some(enabled) = self.network_proxy.as_ref().and_then(FeatureToml::enabled) { entries.insert(Feature::NetworkProxy.key().to_string(), enabled); } @@ -670,6 +683,7 @@ impl FeaturesToml { code_mode, multi_agent_v2, rollout_budget, + current_time_reminder, removed_apps_mcp_path_override: _, network_proxy, entries, @@ -685,6 +699,8 @@ impl FeaturesToml { materialize_resolved_feature_enabled(multi_agent_v2, enabled); } else if spec.id == Feature::RolloutBudget { materialize_resolved_feature_enabled(rollout_budget, enabled); + } else if spec.id == Feature::CurrentTimeReminder { + materialize_resolved_feature_enabled(current_time_reminder, enabled); } else if spec.id == Feature::NetworkProxy { materialize_resolved_feature_enabled(network_proxy, enabled); } else { @@ -1184,6 +1200,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::CurrentTimeReminder, + key: "current_time_reminder", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::SleepTool, key: "sleep_tool", diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index ca11ff907..1534ba86a 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -278,6 +278,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R ghost_snapshot: GhostSnapshotConfig::default(), multi_agent_v2: MultiAgentV2Config::default(), rollout_budget: None, + current_time_reminder: None, features: Default::default(), suppress_unstable_features_warning: false, active_project: ProjectConfig { trust_level: None },