mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
core: persist initial context window metadata (#29519)
## Why PR #29494 made context-window IDs visible to the model by wrapping the token-budget window payload in `<context_window>`, but rollout JSONL consumers still could not see the initial window identity by tailing the session file. Compacted rollout items carry window IDs only after compaction has happened, so a session with no compaction had no durable JSONL record for window 0. This change gives tailing consumers a stable initial-window record at session creation time. ## What Changed - Added `session_meta.context_window.window_id` for the initial context-window identity. - `CreateThreadParams` now requires `initial_window_id: String`, so thread-store callers cannot accidentally create new threads without window-0 metadata. - Live thread creation derives the persisted initial window ID from the same `AutoCompactWindowIds` used to initialize `SessionState`, keeping runtime state and JSONL metadata aligned. - Rollout reconstruction uses `session_meta.context_window.window_id` as the initial-window fallback and derives `window_number = 0`, `first_window_id = window_id`, and `previous_window_id = None` internally. - Fork reconstruction intentionally uses the same rollout reconstruction path; consumers that need to distinguish copied initial-window metadata can use the rollout `thread_id`. - Legacy compactions without `window_number` still use compaction-count fallback accounting instead of being reset to window 0 by the initial-window fallback. - Compacted rollout metadata still takes precedence once compaction records exist, preserving the richer chain fields there. ## JSONL Shape Real rollout JSONL is one object per line. This example is expanded for readability, but shows the new initial `session_meta.context_window` record followed by the existing compacted rollout item shape that also carries window IDs: ```jsonl { "timestamp": "2026-06-22T12:00:00.000Z", "type": "session_meta", "payload": { "session_id": "<THREAD_ID>", "id": "<THREAD_ID>", "timestamp": "2026-06-22T12:00:00.000Z", "cwd": "/repo", "originator": "codex", "cli_version": "0.0.0", "source": "cli", "model_provider": "<MODEL_PROVIDER>", "context_window": { "window_id": "<INITIAL_WINDOW_ID>" } } } ... { "timestamp": "2026-06-22T12:34:56.000Z", "type": "compacted", "payload": { "message": "<COMPACTION_SUMMARY>", "replacement_history": [ "..." ], "window_number": 1, "first_window_id": "<INITIAL_WINDOW_ID>", "previous_window_id": "<INITIAL_WINDOW_ID>", "window_id": "<NEXT_WINDOW_ID>" } } ``` The nested `context_window` object is intentional: it gives rollout consumers a stable namespace for context-window metadata while only writing the non-derivable initial `window_id`. For the initial window, `window_number`, `first_window_id`, and `previous_window_id` are derived internally instead of being written to the rollout. ## Verification - `just test -p codex-protocol` - `just test -p codex-rollout recorder_materializes_on_flush_with_pending_items` - `just test -p codex-core reconstruct_history` - `just test -p codex-core record_initial_history_reconstructs_forked_transcript` - `just test -p codex-thread-store` - `just test -p codex-state` - `just test -p codex-app-server thread_read_returns_summary_without_turns` - `just test -p codex-rollout persistence_metrics`
This commit is contained in:
committed by
GitHub
Unverified
parent
c26f961b85
commit
01f89c8c59
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::context_manager::is_user_turn_boundary;
|
||||
use codex_protocol::protocol::SessionContextWindow;
|
||||
use uuid::Uuid;
|
||||
|
||||
// Return value of `Session::reconstruct_history_from_rollout`, bundling the rebuilt history with
|
||||
@@ -113,6 +114,22 @@ impl Session {
|
||||
// stopping once a surviving replacement-history checkpoint and the required resume metadata
|
||||
// are both known; then replay only the buffered surviving tail forward to preserve exact
|
||||
// history semantics.
|
||||
let has_legacy_compaction_without_window_number =
|
||||
rollout_items.iter().any(|item| {
|
||||
matches!(item, RolloutItem::Compacted(compacted) if compacted.window_number.is_none())
|
||||
});
|
||||
let initial_window = if has_legacy_compaction_without_window_number {
|
||||
None
|
||||
} else {
|
||||
rollout_items.iter().find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(session_meta) => session_meta
|
||||
.meta
|
||||
.context_window
|
||||
.as_ref()
|
||||
.and_then(reconstructed_window_from_session_context_window),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let mut base_replacement_history: Option<&[ResponseItem]> = None;
|
||||
let mut previous_turn_settings = None;
|
||||
let mut reference_context_item = TurnReferenceContextItem::NeverSet;
|
||||
@@ -348,7 +365,7 @@ impl Session {
|
||||
reference_context_item
|
||||
};
|
||||
|
||||
let window = window.unwrap_or(ReconstructedWindow {
|
||||
let window = window.or(initial_window).unwrap_or(ReconstructedWindow {
|
||||
number: fallback_window_number,
|
||||
first_id: None,
|
||||
previous_id: None,
|
||||
@@ -371,3 +388,15 @@ fn parse_uuid_v7(value: &str) -> Option<Uuid> {
|
||||
.ok()
|
||||
.filter(|uuid| uuid.get_version_num() == 7)
|
||||
}
|
||||
|
||||
fn reconstructed_window_from_session_context_window(
|
||||
context_window: &SessionContextWindow,
|
||||
) -> Option<ReconstructedWindow> {
|
||||
let id = parse_uuid_v7(&context_window.window_id)?;
|
||||
Some(ReconstructedWindow {
|
||||
number: 0,
|
||||
first_id: Some(id),
|
||||
previous_id: None,
|
||||
id: Some(id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,8 +9,12 @@ use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::ResumedHistory;
|
||||
use codex_protocol::protocol::SessionContextWindow;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn user_message(text: &str) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
@@ -909,6 +913,116 @@ async fn record_initial_history_resumed_does_not_seed_reference_context_item_aft
|
||||
assert!(session.reference_context_item().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconstruct_history_restores_initial_window_from_session_meta() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let thread_id = ThreadId::default();
|
||||
let initial_window_id = Uuid::now_v7();
|
||||
let rollout_items = vec![RolloutItem::SessionMeta(SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
session_id: thread_id.into(),
|
||||
id: thread_id,
|
||||
context_window: Some(SessionContextWindow {
|
||||
window_id: initial_window_id.to_string(),
|
||||
}),
|
||||
..SessionMeta::default()
|
||||
},
|
||||
git: None,
|
||||
})];
|
||||
|
||||
let reconstructed = session
|
||||
.reconstruct_history_from_rollout(&turn_context, &rollout_items)
|
||||
.await;
|
||||
|
||||
assert_eq!(reconstructed.window_number, 0);
|
||||
assert_eq!(reconstructed.first_window_id, Some(initial_window_id));
|
||||
assert_eq!(reconstructed.previous_window_id, None);
|
||||
assert_eq!(reconstructed.window_id, Some(initial_window_id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconstruct_history_prefers_compacted_window_over_session_meta() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let thread_id = ThreadId::default();
|
||||
let initial_window_id = Uuid::now_v7();
|
||||
let compacted_first_window_id = Uuid::now_v7();
|
||||
let compacted_previous_window_id = Uuid::now_v7();
|
||||
let compacted_window_id = Uuid::now_v7();
|
||||
let rollout_items = vec![
|
||||
RolloutItem::SessionMeta(SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
session_id: thread_id.into(),
|
||||
id: thread_id,
|
||||
context_window: Some(SessionContextWindow {
|
||||
window_id: initial_window_id.to_string(),
|
||||
}),
|
||||
..SessionMeta::default()
|
||||
},
|
||||
git: None,
|
||||
}),
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: Some(Vec::new()),
|
||||
window_number: Some(2),
|
||||
first_window_id: Some(compacted_first_window_id.to_string()),
|
||||
previous_window_id: Some(compacted_previous_window_id.to_string()),
|
||||
window_id: Some(compacted_window_id.to_string()),
|
||||
}),
|
||||
];
|
||||
|
||||
let reconstructed = session
|
||||
.reconstruct_history_from_rollout(&turn_context, &rollout_items)
|
||||
.await;
|
||||
|
||||
assert_eq!(reconstructed.window_number, 2);
|
||||
assert_eq!(
|
||||
reconstructed.first_window_id,
|
||||
Some(compacted_first_window_id)
|
||||
);
|
||||
assert_eq!(
|
||||
reconstructed.previous_window_id,
|
||||
Some(compacted_previous_window_id)
|
||||
);
|
||||
assert_eq!(reconstructed.window_id, Some(compacted_window_id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconstruct_history_preserves_legacy_compaction_count_with_session_meta_window() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let thread_id = ThreadId::default();
|
||||
let initial_window_id = Uuid::now_v7();
|
||||
let rollout_items = vec![
|
||||
RolloutItem::SessionMeta(SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
session_id: thread_id.into(),
|
||||
id: thread_id,
|
||||
context_window: Some(SessionContextWindow {
|
||||
window_id: initial_window_id.to_string(),
|
||||
}),
|
||||
..SessionMeta::default()
|
||||
},
|
||||
git: None,
|
||||
}),
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: "legacy summary".to_string(),
|
||||
replacement_history: None,
|
||||
window_number: None,
|
||||
first_window_id: None,
|
||||
previous_window_id: None,
|
||||
window_id: None,
|
||||
}),
|
||||
];
|
||||
|
||||
let reconstructed = session
|
||||
.reconstruct_history_from_rollout(&turn_context, &rollout_items)
|
||||
.await;
|
||||
|
||||
assert_eq!(reconstructed.window_number, 1);
|
||||
assert_eq!(reconstructed.first_window_id, None);
|
||||
assert_eq!(reconstructed.previous_window_id, None);
|
||||
assert_eq!(reconstructed.window_id, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconstruct_history_legacy_compaction_without_replacement_history_does_not_inject_current_initial_context()
|
||||
{
|
||||
|
||||
@@ -546,6 +546,7 @@ impl Session {
|
||||
SessionId::from(thread_id)
|
||||
}
|
||||
});
|
||||
let initial_auto_compact_window_ids = AutoCompactWindowIds::new_initial();
|
||||
let agent_control = agent_control.with_session_id(
|
||||
session_id,
|
||||
config
|
||||
@@ -585,6 +586,9 @@ impl Session {
|
||||
},
|
||||
dynamic_tools: session_configuration.dynamic_tools.clone(),
|
||||
multi_agent_version: initial_multi_agent_version,
|
||||
initial_window_id: initial_auto_compact_window_ids
|
||||
.window_id
|
||||
.to_string(),
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: Some(config.cwd.to_path_buf()),
|
||||
model_provider: config.model_provider_id.clone(),
|
||||
@@ -889,7 +893,10 @@ impl Session {
|
||||
session_configuration.thread_name = thread_name.clone();
|
||||
validate_config_lock_if_configured(&session_configuration).await?;
|
||||
export_config_lock_if_configured(&session_configuration, thread_id).await?;
|
||||
let state = SessionState::new(session_configuration.clone());
|
||||
let state = SessionState::new_with_auto_compact_window_ids(
|
||||
session_configuration.clone(),
|
||||
initial_auto_compact_window_ids,
|
||||
);
|
||||
let managed_network_requirements_configured = config
|
||||
.config_layer_stack
|
||||
.requirements_toml()
|
||||
|
||||
@@ -3844,6 +3844,7 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
|
||||
base_instructions: BaseInstructions::default(),
|
||||
dynamic_tools: Vec::new(),
|
||||
multi_agent_version: None,
|
||||
initial_window_id: Uuid::now_v7().to_string(),
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: Some(config.cwd.to_path_buf()),
|
||||
model_provider: config.model_provider_id.clone(),
|
||||
@@ -6715,6 +6716,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
|
||||
base_instructions: BaseInstructions::default(),
|
||||
dynamic_tools: Vec::new(),
|
||||
multi_agent_version: None,
|
||||
initial_window_id: Uuid::now_v7().to_string(),
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: Some(config.cwd.to_path_buf()),
|
||||
model_provider: config.model_provider_id.clone(),
|
||||
|
||||
Reference in New Issue
Block a user