core: add UUIDv7 context window IDs (#28953)

## Why

The token-budget context currently identifies a context window by its
thread-local sequence number. A UUIDv7 gives the model a stable opaque
identity that remains fixed for a window and rotates when compaction or
`new_context` starts the next one.

## What changed

- Preserve the existing monotonic value as `window_number` and add a
UUIDv7 `window_id` to `CompactedItem`.
- Generate and rotate the UUID with auto-compaction window state,
persist it alongside the number, and reconstruct it on resume and
rollback.
- Accept legacy compacted rollout records where the numeric `window_id`
represented the window number.
- Use the UUID only in token-budget context; existing request headers
and metadata continue using `thread_id:window_number`.

## Testing

- `just test -p codex-protocol compacted_item::tests`
- `just test -p codex-core token_budget`
This commit is contained in:
pakrym-oai
2026-06-18 17:00:49 -07:00
committed by GitHub
parent e83b7841b0
commit 5c12034e42
19 changed files with 319 additions and 87 deletions
+1
View File
@@ -1105,6 +1105,7 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
RolloutItem::Compacted(CompactedItem {
message: String::new(),
replacement_history: Some(replacement_history),
window_number: None,
window_id: None,
}),
RolloutItem::TurnContext(turn_context.to_turn_context_item()),
+2 -1
View File
@@ -302,7 +302,7 @@ async fn run_compact_task_inner_impl(
let user_messages = collect_user_messages(history_items);
let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);
let window_id = sess.advance_auto_compact_window_id().await;
let (window_number, window_id) = sess.advance_auto_compact_window().await;
if matches!(
initial_context_injection,
@@ -319,6 +319,7 @@ async fn run_compact_task_inner_impl(
let compacted_item = CompactedItem {
message: summary_text.clone(),
replacement_history: Some(new_history.clone()),
window_number: Some(window_number),
window_id: Some(window_id),
};
sess.replace_compacted_history(new_history, reference_context_item, compacted_item)
+2 -1
View File
@@ -258,7 +258,7 @@ async fn run_remote_compact_task_inner_impl(
&responses_metadata,
)
.await?;
let new_window_id = sess.advance_auto_compact_window_id().await;
let (new_window_number, new_window_id) = sess.advance_auto_compact_window().await;
new_history = process_compacted_history(
sess.as_ref(),
turn_context.as_ref(),
@@ -274,6 +274,7 @@ async fn run_remote_compact_task_inner_impl(
let compacted_item = CompactedItem {
message: String::new(),
replacement_history: Some(new_history.clone()),
window_number: Some(new_window_number),
window_id: Some(new_window_id),
};
// Install is the semantic boundary where the compact endpoint's output becomes live
+2 -1
View File
@@ -292,7 +292,7 @@ async fn run_remote_compact_task_inner_impl(
let (compacted_history, retained_images) =
build_v2_compacted_history(&prompt_input, compaction_output);
analytics_details.retained_image_count = Some(retained_images);
let new_window_id = sess.advance_auto_compact_window_id().await;
let (new_window_number, new_window_id) = sess.advance_auto_compact_window().await;
let new_history = process_compacted_history(
sess.as_ref(),
turn_context.as_ref(),
@@ -308,6 +308,7 @@ async fn run_remote_compact_task_inner_impl(
let compacted_item = CompactedItem {
message: String::new(),
replacement_history: Some(new_history.clone()),
window_number: Some(new_window_number),
window_id: Some(new_window_id),
};
compaction_trace.record_installed(&CompactionCheckpointTracePayload {
@@ -1,15 +1,16 @@
use super::ContextualUserFragment;
use codex_protocol::ThreadId;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TokenBudgetContext {
thread_id: ThreadId,
window_id: u64,
window_id: Uuid,
tokens_left: i64,
}
impl TokenBudgetContext {
pub(crate) fn new(thread_id: ThreadId, window_id: u64, tokens_left: i64) -> Self {
pub(crate) fn new(thread_id: ThreadId, window_id: Uuid, tokens_left: i64) -> Self {
Self {
thread_id,
window_id,
@@ -36,7 +37,7 @@ impl ContextualUserFragment for TokenBudgetContext {
let window_id = self.window_id;
let tokens_left = self.tokens_left;
format!(
"Thread id {thread_id}.\nCurrent context window {window_id}.\nYou have {tokens_left} tokens left in this context window."
"Thread id {thread_id}.\nCurrent context window id {window_id}.\nYou have {tokens_left} tokens left in this context window."
)
}
}
+13 -9
View File
@@ -1352,6 +1352,7 @@ impl Session {
mut history,
previous_turn_settings,
reference_context_item,
window_number,
window_id,
} = self
.reconstruct_history_from_rollout(turn_context, rollout_items)
@@ -1370,7 +1371,8 @@ impl Session {
{
let mut state = self.state.lock().await;
state.replace_history(history, reference_context_item);
state.set_auto_compact_window_id(window_id);
let window_id = window_id.unwrap_or_else(|| state.auto_compact_window_id());
state.restore_auto_compact_window(window_number, window_id);
state.set_previous_turn_settings(previous_turn_settings.clone());
}
let prefix_tokens = if matches!(
@@ -3231,13 +3233,14 @@ impl Session {
pub(crate) async fn current_window_id(&self) -> String {
let state = self.state.lock().await;
let thread_id = self.thread_id;
let window_id = state.auto_compact_window_id();
format!("{thread_id}:{window_id}")
let window_number = state.auto_compact_window_number();
format!("{thread_id}:{window_number}")
}
pub(crate) async fn advance_auto_compact_window_id(&self) -> u64 {
pub(crate) async fn advance_auto_compact_window(&self) -> (u64, String) {
let mut state = self.state.lock().await;
state.advance_auto_compact_window_id()
let (window_number, window_id) = state.advance_auto_compact_window();
(window_number, window_id.to_string())
}
pub(crate) async fn request_new_context_window(&self) {
@@ -3249,11 +3252,11 @@ impl Session {
&self,
turn_context: &TurnContext,
) -> Option<u64> {
let window_id = {
let window = {
let mut state = self.state.lock().await;
state.start_new_context_window_if_requested()
};
let window_id = window_id?;
let (window_number, window_id) = window?;
let context_items = self.build_initial_context(turn_context).await;
let turn_context_item = turn_context.to_turn_context_item();
let replacement_history = context_items;
@@ -3265,7 +3268,8 @@ impl Session {
RolloutItem::Compacted(CompactedItem {
message: String::new(),
replacement_history: Some(replacement_history),
window_id: Some(window_id),
window_number: Some(window_number),
window_id: Some(window_id.to_string()),
}),
RolloutItem::TurnContext(turn_context_item),
])
@@ -3275,7 +3279,7 @@ impl Session {
state.queue_pending_session_start_source(codex_hooks::SessionStartSource::Compact);
}
self.recompute_token_usage(turn_context).await;
Some(window_id)
Some(window_number)
}
pub(crate) async fn reference_context_item(&self) -> Option<TurnContextItem> {
@@ -1,5 +1,6 @@
use super::*;
use crate::context_manager::is_user_turn_boundary;
use uuid::Uuid;
// Return value of `Session::reconstruct_history_from_rollout`, bundling the rebuilt history with
// the resume/fork hydration metadata derived from the same replay.
@@ -8,7 +9,14 @@ pub(super) struct RolloutReconstruction {
pub(super) history: Vec<ResponseItem>,
pub(super) previous_turn_settings: Option<PreviousTurnSettings>,
pub(super) reference_context_item: Option<TurnContextItem>,
pub(super) window_id: u64,
pub(super) window_number: u64,
pub(super) window_id: Option<Uuid>,
}
#[derive(Debug, Clone, Copy)]
struct ReconstructedWindow {
number: u64,
id: Option<Uuid>,
}
#[derive(Debug, Default)]
@@ -34,7 +42,7 @@ struct ActiveReplaySegment<'a> {
previous_turn_settings: Option<PreviousTurnSettings>,
reference_context_item: TurnReferenceContextItem,
base_replacement_history: Option<&'a [ResponseItem]>,
window_id: Option<u64>,
window: Option<ReconstructedWindow>,
}
fn turn_ids_are_compatible(active_turn_id: Option<&str>, item_turn_id: Option<&str>) -> bool {
@@ -47,7 +55,7 @@ fn finalize_active_segment<'a>(
base_replacement_history: &mut Option<&'a [ResponseItem]>,
previous_turn_settings: &mut Option<PreviousTurnSettings>,
reference_context_item: &mut TurnReferenceContextItem,
window_id: &mut Option<u64>,
window: &mut Option<ReconstructedWindow>,
pending_rollback_turns: &mut usize,
) {
// Thread rollback drops the newest surviving real user-message boundaries. In replay, that
@@ -68,8 +76,8 @@ fn finalize_active_segment<'a>(
*base_replacement_history = Some(segment_base_replacement_history);
}
if window_id.is_none() {
*window_id = active_segment.window_id;
if window.is_none() {
*window = active_segment.window;
}
// `previous_turn_settings` come from the newest surviving user turn that established them.
@@ -104,7 +112,7 @@ impl Session {
let mut base_replacement_history: Option<&[ResponseItem]> = None;
let mut previous_turn_settings = None;
let mut reference_context_item = TurnReferenceContextItem::NeverSet;
let mut window_id = None;
let mut window = None;
// Rollback is "drop the newest N user turns". While scanning in reverse, that becomes
// "skip the next N user-turn segments we finalize".
let mut pending_rollback_turns = 0usize;
@@ -120,8 +128,13 @@ impl Session {
RolloutItem::Compacted(compacted) => {
let active_segment =
active_segment.get_or_insert_with(ActiveReplaySegment::default);
if active_segment.window_id.is_none() {
active_segment.window_id = compacted.window_id;
if active_segment.window.is_none()
&& let Some(window_number) = compacted.window_number
{
active_segment.window = Some(ReconstructedWindow {
number: window_number,
id: compacted.window_id.as_deref().and_then(parse_uuid_v7),
});
}
// Looking backward, compaction clears any older baseline unless a newer
// `TurnContextItem` in this same segment has already re-established it.
@@ -210,7 +223,7 @@ impl Session {
&mut base_replacement_history,
&mut previous_turn_settings,
&mut reference_context_item,
&mut window_id,
&mut window,
&mut pending_rollback_turns,
);
}
@@ -245,12 +258,12 @@ impl Session {
&mut base_replacement_history,
&mut previous_turn_settings,
&mut reference_context_item,
&mut window_id,
&mut window,
&mut pending_rollback_turns,
);
}
let fallback_window_id = u64::try_from(
let fallback_window_number = u64::try_from(
rollout_items
.iter()
.filter(|item| matches!(item, RolloutItem::Compacted(_)))
@@ -326,11 +339,22 @@ impl Session {
reference_context_item
};
let window = window.unwrap_or(ReconstructedWindow {
number: fallback_window_number,
id: None,
});
RolloutReconstruction {
history: history.raw_items().to_vec(),
previous_turn_settings,
reference_context_item,
window_id: window_id.unwrap_or(fallback_window_id),
window_number: window.number,
window_id: window.id,
}
}
}
fn parse_uuid_v7(value: &str) -> Option<Uuid> {
Uuid::parse_str(value)
.ok()
.filter(|uuid| uuid.get_version_num() == 7)
}
@@ -826,6 +826,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
RolloutItem::Compacted(CompactedItem {
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
window_id: None,
}),
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(
@@ -883,6 +884,7 @@ async fn record_initial_history_resumed_does_not_seed_reference_context_item_aft
RolloutItem::Compacted(CompactedItem {
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
window_id: None,
}),
];
@@ -909,6 +911,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_does_
RolloutItem::Compacted(CompactedItem {
message: "legacy summary".to_string(),
replacement_history: None,
window_number: None,
window_id: None,
}),
];
@@ -941,6 +944,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear
RolloutItem::Compacted(CompactedItem {
message: "legacy summary".to_string(),
replacement_history: None,
window_number: None,
window_id: None,
}),
RolloutItem::EventMsg(EventMsg::TurnStarted(
@@ -1035,6 +1039,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
RolloutItem::Compacted(CompactedItem {
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
window_id: None,
}),
RolloutItem::TurnContext(previous_context_item),
@@ -1185,6 +1190,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
RolloutItem::Compacted(CompactedItem {
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
window_id: None,
}),
];
@@ -1419,6 +1425,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
RolloutItem::Compacted(CompactedItem {
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
window_id: None,
}),
];
@@ -1583,6 +1590,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
RolloutItem::Compacted(CompactedItem {
message: String::new(),
replacement_history: Some(Vec::new()),
window_number: None,
window_id: None,
}),
// A newer TurnStarted replaces the incomplete compacted turn without a matching
+27 -7
View File
@@ -168,6 +168,7 @@ use tokio::sync::Semaphore;
use tokio::time::sleep;
use tokio::time::timeout;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use uuid::Uuid;
use codex_protocol::mcp::CallToolResult as McpCallToolResult;
use pretty_assertions::assert_eq;
@@ -1592,7 +1593,13 @@ async fn reconstruct_history_matches_live_compactions() {
.await;
assert_eq!(expected, reconstructed.history);
assert_eq!(2, reconstructed.window_id);
assert_eq!(2, reconstructed.window_number);
assert_eq!(
reconstructed
.window_id
.map(|window_id| window_id.get_version_num()),
Some(7)
);
}
#[tokio::test]
@@ -1622,10 +1629,12 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
metadata: None,
},
];
let window_id = Uuid::now_v7();
let rollout_items = vec![RolloutItem::Compacted(CompactedItem {
message: String::new(),
replacement_history: Some(replacement_history.clone()),
window_id: Some(42),
window_number: Some(42),
window_id: Some(window_id.to_string()),
})];
let reconstructed = session
@@ -1633,7 +1642,8 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
.await;
assert_eq!(reconstructed.history, replacement_history);
assert_eq!(42, reconstructed.window_id);
assert_eq!(42, reconstructed.window_number);
assert_eq!(Some(window_id), reconstructed.window_id);
}
#[tokio::test]
@@ -3014,6 +3024,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
user_message("turn 1 user"),
user_message("summary after compaction"),
];
let compacted_window_id = Uuid::now_v7();
sess.persist_rollout_items(&[
RolloutItem::EventMsg(EventMsg::TurnStarted(
@@ -3055,7 +3066,8 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
RolloutItem::Compacted(CompactedItem {
message: "summary after compaction".to_string(),
replacement_history: Some(compacted_history.clone()),
window_id: Some(7),
window_number: Some(7),
window_id: Some(compacted_window_id.to_string()),
}),
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: compact_turn_id,
@@ -3105,7 +3117,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
.await;
{
let mut state = sess.state.lock().await;
state.set_auto_compact_window_id(/*window_id*/ 99);
state.restore_auto_compact_window(/*window_number*/ 99, Uuid::now_v7());
}
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
@@ -3114,6 +3126,10 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
assert_eq!(sess.clone_history().await.raw_items(), compacted_history);
assert!(sess.reference_context_item().await.is_none());
assert_eq!(
sess.state.lock().await.auto_compact_window_id(),
compacted_window_id
);
assert!(sess.current_window_id().await.ends_with(":7"));
}
@@ -9846,10 +9862,12 @@ async fn sample_rollout(
let user_messages1 = collect_user_messages(&snapshot1);
let rebuilt1 = compact::build_compacted_history(Vec::new(), &user_messages1, summary1);
live_history.replace(rebuilt1);
let (window_number, window_id) = session.advance_auto_compact_window().await;
rollout_items.push(RolloutItem::Compacted(CompactedItem {
message: summary1.to_string(),
replacement_history: None,
window_id: None,
window_number: Some(window_number),
window_id: Some(window_id),
}));
let user2 = ResponseItem::Message {
@@ -9889,10 +9907,12 @@ async fn sample_rollout(
let user_messages2 = collect_user_messages(&snapshot2);
let rebuilt2 = compact::build_compacted_history(Vec::new(), &user_messages2, summary2);
live_history.replace(rebuilt2);
let (window_number, window_id) = session.advance_auto_compact_window().await;
rollout_items.push(RolloutItem::Compacted(CompactedItem {
message: summary2.to_string(),
replacement_history: None,
window_id: None,
window_number: Some(window_number),
window_id: Some(window_id),
}));
let user3 = ResponseItem::Message {
+28 -12
View File
@@ -1,4 +1,5 @@
use codex_protocol::protocol::TokenUsage;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct AutoCompactWindowSnapshot {
@@ -13,7 +14,8 @@ enum AutoCompactWindowPrefill {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct AutoCompactWindow {
window_id: u64,
window_number: u64,
window_id: Uuid,
new_context_window_requested: bool,
/// Absolute input-token baseline for the current compaction window.
///
@@ -26,7 +28,8 @@ pub(super) struct AutoCompactWindow {
impl AutoCompactWindow {
pub(super) fn new() -> Self {
Self {
window_id: 0,
window_number: 0,
window_id: Uuid::now_v7(),
new_context_window_requested: false,
prefill_input_tokens: None,
}
@@ -36,18 +39,24 @@ impl AutoCompactWindow {
self.prefill_input_tokens = None;
}
pub(super) fn window_id(&self) -> u64 {
pub(super) fn window_number(&self) -> u64 {
self.window_number
}
pub(super) fn window_id(&self) -> Uuid {
self.window_id
}
pub(super) fn set_window_id(&mut self, window_id: u64) {
pub(super) fn restore(&mut self, window_number: u64, window_id: Uuid) {
self.window_number = window_number;
self.window_id = window_id;
}
pub(super) fn advance_window_id(&mut self) -> u64 {
self.window_id = self.window_id.saturating_add(1);
pub(super) fn advance(&mut self) -> (u64, Uuid) {
self.window_number = self.window_number.saturating_add(1);
self.window_id = Uuid::now_v7();
self.new_context_window_requested = false;
self.window_id
(self.window_number, self.window_id)
}
pub(super) fn request_new_context_window(&mut self) {
@@ -108,15 +117,22 @@ mod tests {
fn tracks_prefill_and_window_boundaries() {
let mut window = AutoCompactWindow::new();
assert_eq!(window.window_id(), 0);
window.set_window_id(/*window_id*/ 3);
assert_eq!(window.window_id(), 3);
assert_eq!(window.window_number(), 0);
assert_eq!(window.window_id().get_version_num(), 7);
let restored_window_id = Uuid::now_v7();
window.restore(/*window_number*/ 3, restored_window_id);
assert_eq!(window.window_number(), 3);
assert_eq!(window.window_id(), restored_window_id);
window.request_new_context_window();
assert!(window.take_new_context_window_request());
assert!(!window.take_new_context_window_request());
window.request_new_context_window();
assert_eq!(window.advance_window_id(), 4);
assert_eq!(window.window_id(), 4);
let (window_number, window_id) = window.advance();
assert_eq!(window_number, 4);
assert_eq!(window.window_number(), 4);
assert_eq!(window.window_id(), window_id);
assert_eq!(window_id.get_version_num(), 7);
assert_ne!(window_id, restored_window_id);
assert!(!window.take_new_context_window_request());
assert_eq!(
+13 -8
View File
@@ -6,6 +6,7 @@ use codex_sandboxing::policy_transforms::merge_permission_profiles;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::VecDeque;
use uuid::Uuid;
use super::AdditionalContextStore;
use super::auto_compact_window::AutoCompactWindow;
@@ -147,30 +148,34 @@ impl SessionState {
self.auto_compact_window.snapshot()
}
pub(crate) fn auto_compact_window_id(&self) -> u64 {
pub(crate) fn auto_compact_window_number(&self) -> u64 {
self.auto_compact_window.window_number()
}
pub(crate) fn auto_compact_window_id(&self) -> Uuid {
self.auto_compact_window.window_id()
}
pub(crate) fn set_auto_compact_window_id(&mut self, window_id: u64) {
self.auto_compact_window.set_window_id(window_id);
pub(crate) fn restore_auto_compact_window(&mut self, window_number: u64, window_id: Uuid) {
self.auto_compact_window.restore(window_number, window_id);
}
pub(crate) fn advance_auto_compact_window_id(&mut self) -> u64 {
self.auto_compact_window.advance_window_id()
pub(crate) fn advance_auto_compact_window(&mut self) -> (u64, Uuid) {
self.auto_compact_window.advance()
}
pub(crate) fn request_new_context_window(&mut self) {
self.auto_compact_window.request_new_context_window();
}
pub(crate) fn start_new_context_window_if_requested(&mut self) -> Option<u64> {
pub(crate) fn start_new_context_window_if_requested(&mut self) -> Option<(u64, Uuid)> {
if !self.auto_compact_window.take_new_context_window_request() {
return None;
}
let window_id = self.auto_compact_window.advance_window_id();
let window = self.auto_compact_window.advance();
self.auto_compact_window.clear_prefill();
Some(window_id)
Some(window)
}
pub(crate) fn token_info(&self) -> Option<TokenUsageInfo> {