mirror of
https://github.com/openai/codex.git
synced 2026-10-28 00:03:58 +00:00
## Description This PR makes new threads with `history_mode = "paginated"` persist `ItemCompleted(item: <turn_item>)` in their rollout JSONL file. Legacy threads keep persisting the existing legacy events. Because the format is selected per thread, a rollout is either legacy or paginated; we do not need to support mixed rollouts containing both representations. This PR depends on [#31473](https://github.com/openai/codex/pull/31473). ## Why Paginated thread history needs stable turn/item IDs and completed item snapshots so the later SQLite projector can materialize appended rollout JSONL without rebuilding the whole thread. Keeping the legacy persistence policy unchanged avoids changing historical rollouts or the readers that still consume them. ## What changed - Made rollout filtering history-mode aware. Paginated threads keep completed canonical `ItemCompleted` events and drop their redundant legacy projections; legacy threads keep the existing event set. - Made forks inherit the source thread history mode, so copied legacy history is never filtered as paginated. - Made paginated threads assign IDs to locally-created response items even when `Feature::ItemIds` is off, and reject streamed output items that arrive without server IDs. - Updated legacy turn replay, rollout list/search, and SQLite metadata extraction to understand completed canonical user-message items.
180 lines
7.5 KiB
Rust
180 lines
7.5 KiB
Rust
use super::*;
|
||
use std::sync::atomic::AtomicBool;
|
||
|
||
/// Spawn a review thread using the given prompt.
|
||
pub(super) async fn spawn_review_thread(
|
||
sess: Arc<Session>,
|
||
config: Arc<Config>,
|
||
parent_turn_context: Arc<TurnContext>,
|
||
sub_id: String,
|
||
resolved: crate::review_prompts::ResolvedReviewRequest,
|
||
) {
|
||
let model = config
|
||
.review_model
|
||
.clone()
|
||
.unwrap_or_else(|| parent_turn_context.model_info.slug.clone());
|
||
let review_model_info = sess
|
||
.services
|
||
.models_manager
|
||
.get_model_info(&model, &config.to_models_manager_config())
|
||
.await;
|
||
// For reviews, disable web_search and view_image regardless of global settings.
|
||
let mut review_features = sess.features.clone();
|
||
let _ = review_features.disable(Feature::WebSearchRequest);
|
||
let _ = review_features.disable(Feature::WebSearchCached);
|
||
let _ = review_features.disable(Feature::Goals);
|
||
let review_web_search_mode = WebSearchMode::Disabled;
|
||
let available_models = sess
|
||
.services
|
||
.models_manager
|
||
.list_models(
|
||
RefreshStrategy::OnlineIfUncached,
|
||
config.http_client_factory(),
|
||
)
|
||
.await;
|
||
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
|
||
codex_tools::unified_exec_feature_mode_for_features(review_features.get()),
|
||
crate::tools::tool_user_shell_type(sess.services.user_shell.as_ref()),
|
||
sess.services.shell_zsh_path.as_ref(),
|
||
sess.services.main_execve_wrapper_exe.as_ref(),
|
||
);
|
||
|
||
let review_prompt = resolved.prompt.clone();
|
||
let provider = parent_turn_context.provider.clone();
|
||
let auth_manager = parent_turn_context.auth_manager.clone();
|
||
let model_info = review_model_info.clone();
|
||
|
||
// Build per‑turn client with the requested model/family.
|
||
let mut per_turn_config = (*config).clone();
|
||
per_turn_config.model = Some(model.clone());
|
||
per_turn_config.features = review_features.clone();
|
||
per_turn_config.permissions.shell_environment_policy = parent_turn_context
|
||
.config
|
||
.permissions
|
||
.shell_environment_policy
|
||
.clone();
|
||
per_turn_config.codex_linux_sandbox_exe =
|
||
parent_turn_context.config.codex_linux_sandbox_exe.clone();
|
||
per_turn_config.compact_prompt = parent_turn_context.config.compact_prompt.clone();
|
||
if let Err(err) = per_turn_config.web_search_mode.set(review_web_search_mode) {
|
||
let fallback_value = per_turn_config.web_search_mode.value();
|
||
tracing::warn!(
|
||
error = %err,
|
||
?review_web_search_mode,
|
||
?fallback_value,
|
||
"review web_search_mode is disallowed by requirements; keeping constrained value"
|
||
);
|
||
}
|
||
|
||
let session_telemetry = parent_turn_context
|
||
.session_telemetry
|
||
.clone()
|
||
.with_model(model.as_str(), review_model_info.slug.as_str());
|
||
let auth_manager_for_context = auth_manager.clone();
|
||
let provider_for_context = provider.clone();
|
||
let session_telemetry_for_context = session_telemetry.clone();
|
||
let reasoning_effort = per_turn_config.model_reasoning_effort.clone();
|
||
let reasoning_summary = per_turn_config
|
||
.model_reasoning_summary
|
||
.unwrap_or(model_info.default_reasoning_summary);
|
||
let session_source = parent_turn_context.session_source.clone();
|
||
let (forked_from_thread_id, thread_source) = {
|
||
let state = sess.state.lock().await;
|
||
(
|
||
state.session_configuration.forked_from_thread_id,
|
||
state.session_configuration.thread_source.clone(),
|
||
)
|
||
};
|
||
|
||
let per_turn_config = Arc::new(per_turn_config);
|
||
let review_turn_id = sub_id.to_string();
|
||
let turn_metadata_state = Arc::new(TurnMetadataState::new(
|
||
sess.session_id().to_string(),
|
||
sess.thread_id().to_string(),
|
||
forked_from_thread_id,
|
||
parent_turn_context.parent_thread_id,
|
||
&session_source,
|
||
thread_source,
|
||
review_turn_id.clone(),
|
||
#[allow(deprecated)]
|
||
parent_turn_context.cwd.clone(),
|
||
&parent_turn_context.permission_profile,
|
||
parent_turn_context.windows_sandbox_level,
|
||
parent_turn_context.network.is_some(),
|
||
));
|
||
|
||
let extension_data = Arc::new(codex_extension_api::ExtensionData::new(
|
||
review_turn_id.clone(),
|
||
));
|
||
extension_data.insert(parent_turn_context.turn_skills.snapshot.clone());
|
||
|
||
let review_turn_context = TurnContext {
|
||
sub_id: review_turn_id.clone(),
|
||
trace_id: current_span_trace_id(),
|
||
realtime_active: parent_turn_context.realtime_active,
|
||
config: per_turn_config,
|
||
auth_manager: auth_manager_for_context,
|
||
model_info: model_info.clone(),
|
||
session_telemetry: session_telemetry_for_context,
|
||
provider: provider_for_context,
|
||
reasoning_effort,
|
||
reasoning_summary,
|
||
session_source,
|
||
history_mode: parent_turn_context.history_mode,
|
||
parent_thread_id: parent_turn_context.parent_thread_id,
|
||
originator: parent_turn_context.originator.clone(),
|
||
environments: parent_turn_context.environments.clone(),
|
||
available_models,
|
||
unified_exec_shell_mode,
|
||
current_date: parent_turn_context.current_date.clone(),
|
||
timezone: parent_turn_context.timezone.clone(),
|
||
app_server_client_name: parent_turn_context.app_server_client_name.clone(),
|
||
developer_instructions: None,
|
||
collaboration_mode: parent_turn_context.collaboration_mode.clone(),
|
||
multi_agent_version: MultiAgentVersion::Disabled,
|
||
personality: parent_turn_context.personality,
|
||
approval_policy: parent_turn_context.approval_policy.clone(),
|
||
permission_profile: parent_turn_context.permission_profile(),
|
||
network: parent_turn_context.network.clone(),
|
||
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
|
||
#[allow(deprecated)]
|
||
cwd: parent_turn_context.cwd.clone(),
|
||
final_output_json_schema: None,
|
||
dynamic_tools: parent_turn_context.dynamic_tools.clone(),
|
||
turn_metadata_state,
|
||
extension_data,
|
||
turn_skills: TurnSkillsContext::new(parent_turn_context.turn_skills.snapshot.clone()),
|
||
turn_timing_state: Arc::new(TurnTimingState::default()),
|
||
terminal_error: Arc::new(Mutex::new(None)),
|
||
server_model_warning_emitted: AtomicBool::new(false),
|
||
model_verification_emitted: AtomicBool::new(false),
|
||
};
|
||
|
||
// Seed the child task with the review prompt as the initial user message.
|
||
let input = vec![TurnInput::UserInput {
|
||
content: vec![UserInput::Text {
|
||
text: review_prompt,
|
||
// Review prompt is synthesized; no UI element ranges to preserve.
|
||
text_elements: Vec::new(),
|
||
}],
|
||
client_id: None,
|
||
}];
|
||
let tc = Arc::new(review_turn_context);
|
||
if tc.environments.single_local_environment_cwd().is_some() {
|
||
tc.turn_metadata_state.spawn_git_enrichment_task();
|
||
}
|
||
// TODO(ccunningham): Review turns currently rely on `spawn_task` for TurnComplete but do not
|
||
// emit a parent TurnStarted. Consider giving review a full parent turn lifecycle
|
||
// (TurnStarted + TurnComplete) for consistency with other standalone tasks.
|
||
sess.spawn_task(tc.clone(), input, ReviewTask::new()).await;
|
||
|
||
// Announce entering review mode so UIs can switch modes.
|
||
let item = TurnItem::EnteredReviewMode(EnteredReviewModeItem {
|
||
id: uuid::Uuid::now_v7().to_string(),
|
||
target: resolved.target,
|
||
user_facing_hint: resolved.user_facing_hint,
|
||
});
|
||
sess.emit_turn_item_started(&tc, &item).await;
|
||
sess.emit_turn_item_completed(&tc, item).await;
|
||
}
|