fix: switch rate limit reset handling to timestamps (#5304)
This change ensures that we store the absolute time instead of relative offsets of when the primary and secondary rate limits will reset. Previously these got recalculated relative to current time, which leads to the displayed reset times to change over time, including after doing a codex resume. For previously changed sessions, this will cause the reset times to not show due to this being a breaking change: <img width="524" height="55" alt="Screenshot 2025-10-17 at 5 14 18 PM" src="https://github.com/user-attachments/assets/53ebd43e-da25-4fef-9c47-94a529d40265" /> Fixes https://github.com/openai/codex/issues/4761
This commit is contained in:
committed by
GitHub
parent
41900e9d0f
commit
0e08dd6055
@@ -2,11 +2,9 @@ use crate::chatwidget::get_limits_duration;
|
||||
|
||||
use super::helpers::format_reset_timestamp;
|
||||
use chrono::DateTime;
|
||||
use chrono::Duration as ChronoDuration;
|
||||
use chrono::Local;
|
||||
use codex_core::protocol::RateLimitSnapshot;
|
||||
use codex_core::protocol::RateLimitWindow;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
const STATUS_LIMIT_BAR_SEGMENTS: usize = 20;
|
||||
const STATUS_LIMIT_BAR_FILLED: &str = "█";
|
||||
@@ -35,9 +33,10 @@ pub(crate) struct RateLimitWindowDisplay {
|
||||
impl RateLimitWindowDisplay {
|
||||
fn from_window(window: &RateLimitWindow, captured_at: DateTime<Local>) -> Self {
|
||||
let resets_at = window
|
||||
.resets_in_seconds
|
||||
.and_then(|seconds| i64::try_from(seconds).ok())
|
||||
.and_then(|secs| captured_at.checked_add_signed(ChronoDuration::seconds(secs)))
|
||||
.resets_at
|
||||
.as_deref()
|
||||
.and_then(|value| DateTime::parse_from_rfc3339(value).ok())
|
||||
.map(|dt| dt.with_timezone(&Local))
|
||||
.map(|dt| format_reset_timestamp(dt, captured_at));
|
||||
|
||||
Self {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use super::new_status_output;
|
||||
use super::rate_limit_snapshot_display;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use chrono::Duration as ChronoDuration;
|
||||
use chrono::TimeZone;
|
||||
use chrono::Utc;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::ConfigToml;
|
||||
@@ -60,6 +62,12 @@ fn sanitize_directory(lines: Vec<String>) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn reset_at_from(captured_at: &chrono::DateTime<chrono::Local>, seconds: i64) -> String {
|
||||
(*captured_at + ChronoDuration::seconds(seconds))
|
||||
.with_timezone(&Utc)
|
||||
.to_rfc3339()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_snapshot_includes_reasoning_details() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
@@ -85,22 +93,22 @@ fn status_snapshot_includes_reasoning_details() {
|
||||
total_tokens: 2_250,
|
||||
};
|
||||
|
||||
let snapshot = RateLimitSnapshot {
|
||||
primary: Some(RateLimitWindow {
|
||||
used_percent: 72.5,
|
||||
window_minutes: Some(300),
|
||||
resets_in_seconds: Some(600),
|
||||
}),
|
||||
secondary: Some(RateLimitWindow {
|
||||
used_percent: 45.0,
|
||||
window_minutes: Some(10080),
|
||||
resets_in_seconds: Some(1_200),
|
||||
}),
|
||||
};
|
||||
let captured_at = chrono::Local
|
||||
.with_ymd_and_hms(2024, 1, 2, 3, 4, 5)
|
||||
.single()
|
||||
.expect("timestamp");
|
||||
let snapshot = RateLimitSnapshot {
|
||||
primary: Some(RateLimitWindow {
|
||||
used_percent: 72.5,
|
||||
window_minutes: Some(300),
|
||||
resets_at: Some(reset_at_from(&captured_at, 600)),
|
||||
}),
|
||||
secondary: Some(RateLimitWindow {
|
||||
used_percent: 45.0,
|
||||
window_minutes: Some(10080),
|
||||
resets_at: Some(reset_at_from(&captured_at, 1_200)),
|
||||
}),
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
|
||||
let composite = new_status_output(&config, &usage, Some(&usage), &None, Some(&rate_display));
|
||||
@@ -130,18 +138,18 @@ fn status_snapshot_includes_monthly_limit() {
|
||||
total_tokens: 1_200,
|
||||
};
|
||||
|
||||
let snapshot = RateLimitSnapshot {
|
||||
primary: Some(RateLimitWindow {
|
||||
used_percent: 12.0,
|
||||
window_minutes: Some(43_200),
|
||||
resets_in_seconds: Some(86_400),
|
||||
}),
|
||||
secondary: None,
|
||||
};
|
||||
let captured_at = chrono::Local
|
||||
.with_ymd_and_hms(2024, 5, 6, 7, 8, 9)
|
||||
.single()
|
||||
.expect("timestamp");
|
||||
let snapshot = RateLimitSnapshot {
|
||||
primary: Some(RateLimitWindow {
|
||||
used_percent: 12.0,
|
||||
window_minutes: Some(43_200),
|
||||
resets_at: Some(reset_at_from(&captured_at, 86_400)),
|
||||
}),
|
||||
secondary: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
|
||||
let composite = new_status_output(&config, &usage, Some(&usage), &None, Some(&rate_display));
|
||||
@@ -197,18 +205,18 @@ fn status_snapshot_truncates_in_narrow_terminal() {
|
||||
total_tokens: 2_250,
|
||||
};
|
||||
|
||||
let snapshot = RateLimitSnapshot {
|
||||
primary: Some(RateLimitWindow {
|
||||
used_percent: 72.5,
|
||||
window_minutes: Some(300),
|
||||
resets_in_seconds: Some(600),
|
||||
}),
|
||||
secondary: None,
|
||||
};
|
||||
let captured_at = chrono::Local
|
||||
.with_ymd_and_hms(2024, 1, 2, 3, 4, 5)
|
||||
.single()
|
||||
.expect("timestamp");
|
||||
let snapshot = RateLimitSnapshot {
|
||||
primary: Some(RateLimitWindow {
|
||||
used_percent: 72.5,
|
||||
window_minutes: Some(300),
|
||||
resets_at: Some(reset_at_from(&captured_at, 600)),
|
||||
}),
|
||||
secondary: None,
|
||||
};
|
||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||
|
||||
let composite = new_status_output(&config, &usage, Some(&usage), &None, Some(&rate_display));
|
||||
|
||||
Reference in New Issue
Block a user