Files
codex/codex-rs/core/src/session/time_reminder.rs
T
Abhinav b64de2f3ad Use the originating model when recording conversation history (#44243)
## Why

History recording used the model captured at turn start, so switching models within a turn could apply stale image preparation and tool-output truncation rules.

## What changed

Pass explicit model metadata through conversation recording. Record model responses and tool results using their originating step's settings, and capture the current model for pending input recorded during startup interruption or task completion.

## Testing

Add regression coverage for tool results across a model switch, explicit model selection for plain and annotated history, and image input retained during interruption and completion. Extend model-switching tests to verify that prepared media and item identity survive switching back to a multimodal model in live, resumed, and forked threads.

GitOrigin-RevId: 5bc750c30c334acb9b802cb80fdea01daffa84a1
2026-09-09 17:46:00 +00:00

144 lines
4.4 KiB
Rust

use chrono::DateTime;
use chrono::Utc;
use codex_features::CurrentTimeReminderDeliveryMode;
use codex_features::Feature;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::ResponseItem;
use super::session::Session;
use super::turn_context::TurnContext;
use crate::config::Config;
use crate::config::CurrentTimeReminderConfig;
use crate::context::ContextualUserFragment;
use crate::context_manager::is_user_turn_boundary;
pub(super) fn apply_persistent_defaults(config: &mut Config) {
if config.current_time_reminder.is_some()
|| config
.config_layer_stack
.effective_config()
.get("features")
.and_then(|features| features.get("current_time_reminder"))
.is_some()
{
return;
}
// Apply defaults only to this turn; explicit settings and managed policy win.
if config.features.enable(Feature::CurrentTimeReminder).is_ok()
&& config.features.enabled(Feature::CurrentTimeReminder)
{
config.current_time_reminder = Some(CurrentTimeReminderConfig {
sleep_tool: true,
..CurrentTimeReminderConfig::default()
});
}
}
#[derive(Default)]
pub(crate) struct CurrentTimeReminderState {
last_delivery_time: Option<DateTime<Utc>>,
last_window_id: Option<String>,
pending_user_or_tool_output_boundary: bool,
}
impl CurrentTimeReminderState {
pub(super) fn note_recorded_items(&mut self, items: &[ResponseItem]) {
if items.iter().any(|item| {
is_user_turn_boundary(item)
|| matches!(
item,
ResponseItem::FunctionCallOutput { .. }
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::ToolSearchOutput { .. }
)
}) {
self.pending_user_or_tool_output_boundary = true;
}
}
fn take_reminder_due(
&mut self,
window_id: &str,
current_time: DateTime<Utc>,
interval_seconds: u64,
delivery_mode: CurrentTimeReminderDeliveryMode,
) -> bool {
let is_new_window = self.last_window_id.as_deref() != Some(window_id);
// Consume the boundary for this inference even if the interval suppresses delivery.
let follows_user_or_tool_output =
std::mem::take(&mut self.pending_user_or_tool_output_boundary);
if delivery_mode == CurrentTimeReminderDeliveryMode::AfterUserOrToolOutput
&& !is_new_window
&& !follows_user_or_tool_output
{
return false;
}
let reminder_is_due = is_new_window
|| interval_seconds == 0
|| self.last_delivery_time.is_none_or(|last_delivery_time| {
current_time
.signed_duration_since(last_delivery_time)
.num_seconds()
>= i64::try_from(interval_seconds).unwrap_or(i64::MAX)
});
if reminder_is_due {
self.last_delivery_time = Some(current_time);
self.last_window_id = Some(window_id.to_string());
}
reminder_is_due
}
}
pub(super) async fn maybe_record_current_time_reminder(
sess: &Session,
turn_context: &TurnContext,
window_id: &str,
) -> CodexResult<()> {
if !turn_context
.config
.features
.enabled(Feature::CurrentTimeReminder)
{
return Ok(());
}
let Some(config) = turn_context.config.current_time_reminder else {
return Ok(());
};
let current_time = sess
.services
.time_provider
.current_time(sess.thread_id)
.await
.map_err(|err| CodexErr::Fatal(format!("failed to read current time: {err:#}")))?;
let reminder_is_due = {
let mut state = sess.state.lock().await;
state.current_time_reminder.take_reminder_due(
window_id,
current_time,
config.reminder_interval_seconds,
config.delivery_mode,
)
};
if !reminder_is_due {
return Ok(());
}
let response_item =
ContextualUserFragment::into(crate::context::CurrentTimeReminder::new(current_time));
sess.record_conversation_items(
turn_context,
turn_context.model_info(),
std::slice::from_ref(&response_item),
)
.await;
Ok(())
}