mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Add experimental prediction forks that inherit parent context (#51884)
## Why Prediction forks need to preserve the parent's effective context and request settings to maximize prompt-cache reuse. ## What changed - Add `experimentalPredictionMode` to `thread/fork` for ephemeral forks of loaded, persisted parents. Reject configuration overrides, `path`, `lastTurnId`, and `beforeTurnId` in this mode. - Inherit the parent's effective configuration, tools, environments, collaboration settings, disabled plugins, and persisted history. - Enable `features.reasoning_effort_override` on the fork while preserving the parent's request-effort baseline, including when overrides were disabled on the parent. - Report the fork's effective model, service tier, and reasoning effort in the response. ## Testing Add coverage for matching parent and fork requests and prompt-cache keys across legacy and paginated history, invalid fork parameters, and reasoning-effort inheritance. Add a context snapshot for identical parent and fork continuations. GitOrigin-RevId: 46b41f2ac331562b441dadff1a0a9f926c94880d
This commit is contained in:
@@ -4890,8 +4890,34 @@ impl ThreadRequestProcessor {
|
||||
app_server_client_version: Option<String>,
|
||||
client_mcp_extensions: ClientMcpExtensions,
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let has_config_overrides = params.model.is_some()
|
||||
|| params.model_provider.is_some()
|
||||
|| params.service_tier.is_some()
|
||||
|| params.cwd.is_some()
|
||||
|| params.runtime_workspace_roots.is_some()
|
||||
|| params.approval_policy.is_some()
|
||||
|| params.approvals_reviewer.is_some()
|
||||
|| params.sandbox.is_some()
|
||||
|| params.permissions.is_some()
|
||||
|| params.base_instructions.is_some()
|
||||
|| params.developer_instructions.is_some()
|
||||
|| params
|
||||
.config
|
||||
.as_ref()
|
||||
.is_some_and(|config| !config.is_empty());
|
||||
if params.experimental_prediction_mode && has_config_overrides {
|
||||
return Err(invalid_request(
|
||||
"`experimentalPredictionMode` cannot be combined with configuration overrides",
|
||||
));
|
||||
}
|
||||
if params.experimental_prediction_mode && !params.ephemeral {
|
||||
return Err(invalid_request(
|
||||
"`experimentalPredictionMode` requires `ephemeral: true`",
|
||||
));
|
||||
}
|
||||
let ThreadForkParams {
|
||||
thread_id,
|
||||
experimental_prediction_mode,
|
||||
last_turn_id,
|
||||
before_turn_id,
|
||||
path,
|
||||
@@ -4918,6 +4944,13 @@ impl ThreadRequestProcessor {
|
||||
"`permissions` cannot be combined with `sandbox`",
|
||||
));
|
||||
}
|
||||
if experimental_prediction_mode
|
||||
&& (path.is_some() || last_turn_id.is_some() || before_turn_id.is_some())
|
||||
{
|
||||
return Err(invalid_request(
|
||||
"`experimentalPredictionMode` requires a thread id without a path or turn cutoff",
|
||||
));
|
||||
}
|
||||
let source_thread = self
|
||||
.read_stored_thread_for_resume(
|
||||
&thread_id,
|
||||
@@ -4925,6 +4958,31 @@ impl ThreadRequestProcessor {
|
||||
/*include_history*/ false,
|
||||
)
|
||||
.await?;
|
||||
let inherited_fork = if experimental_prediction_mode {
|
||||
// Keep inherited state and its preparation off the fork handler's stack.
|
||||
let (mut options, settings) = Box::pin(async {
|
||||
self.thread_manager
|
||||
.fork_options_from_parent(source_thread.thread_id)
|
||||
.await
|
||||
.map(|(options, settings)| (Box::new(options), Box::new(settings)))
|
||||
})
|
||||
.await
|
||||
.map_err(|err| match err.details() {
|
||||
CodexErrorDetails::ThreadNotFound(_) => {
|
||||
invalid_request("`experimentalPredictionMode` requires a loaded parent")
|
||||
}
|
||||
_ => internal_error(format!("failed to inherit parent state: {err}")),
|
||||
})?;
|
||||
options.config.ephemeral = ephemeral;
|
||||
options
|
||||
.config
|
||||
.features
|
||||
.enable(Feature::ReasoningEffortOverride)
|
||||
.map_err(|err| invalid_request(err.to_string()))?;
|
||||
Some((options, settings))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let paginated_source = matches!(source_thread.history_mode, ThreadHistoryMode::Paginated);
|
||||
if last_turn_id.is_some() && before_turn_id.is_some() {
|
||||
return Err(invalid_request(
|
||||
@@ -5011,7 +5069,7 @@ impl ThreadRequestProcessor {
|
||||
|
||||
// Persist Windows sandbox mode.
|
||||
let mut cli_overrides = cli_overrides.unwrap_or_default();
|
||||
if cfg!(windows) {
|
||||
if cfg!(windows) && !experimental_prediction_mode {
|
||||
let mode = self.config.permissions.windows_sandbox_mode.or_else(|| {
|
||||
match WindowsSandboxLevel::from_config(&self.config) {
|
||||
WindowsSandboxLevel::Elevated => Some(WindowsSandboxModeToml::Elevated),
|
||||
@@ -5122,15 +5180,24 @@ impl ThreadRequestProcessor {
|
||||
.map(|profile| profile.id);
|
||||
}
|
||||
}
|
||||
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
|
||||
let config = self
|
||||
.config_manager
|
||||
.load_for_cwd(request_overrides, typesafe_overrides, history_cwd)
|
||||
.await
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
let goals_enabled = config.features.enabled(Feature::Goals);
|
||||
let (inherited_options, inherited_settings) = inherited_fork.unzip();
|
||||
let options = if let Some(options) = inherited_options {
|
||||
*options
|
||||
} else {
|
||||
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
|
||||
let config = self
|
||||
.config_manager
|
||||
.load_for_cwd(request_overrides, typesafe_overrides, history_cwd)
|
||||
.await
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
StartThreadOptions {
|
||||
client_mcp_extensions,
|
||||
..StartThreadOptions::new(config)
|
||||
}
|
||||
};
|
||||
let goals_enabled = options.config.features.enabled(Feature::Goals);
|
||||
|
||||
let fallback_model_provider = config.model_provider_id.clone();
|
||||
let fallback_model_provider = options.config.model_provider_id.clone();
|
||||
let parent_trace = self.request_trace_context(&request_id).await;
|
||||
let thread_source = thread_source.map(Into::into);
|
||||
|
||||
@@ -5186,7 +5253,7 @@ impl ThreadRequestProcessor {
|
||||
.then(|| restored_token_usage_turn_id(&history_items, ephemeral_turns.as_slice()));
|
||||
let token_usage_history_items = paginated_source.then(|| Arc::clone(&history_items));
|
||||
let inherited_project_id = source_thread.project_id.clone();
|
||||
let reserved_thread_id = if config.ephemeral {
|
||||
let reserved_thread_id = if options.config.ephemeral {
|
||||
None
|
||||
} else {
|
||||
stage_pending_thread_metadata(
|
||||
@@ -5205,9 +5272,8 @@ impl ThreadRequestProcessor {
|
||||
let fork_options = StartThreadOptions {
|
||||
thread_source,
|
||||
parent_trace,
|
||||
client_mcp_extensions,
|
||||
reserved_thread_id,
|
||||
..StartThreadOptions::new(config)
|
||||
..options
|
||||
};
|
||||
let new_thread = if let Some(prepared_fork) = prepared_fork {
|
||||
self.thread_manager
|
||||
@@ -5247,6 +5313,13 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(settings) = inherited_settings {
|
||||
// Construct the restore future off the fork handler's stack
|
||||
Box::pin(async { Box::pin(forked_thread.restore_thread_settings(*settings)).await })
|
||||
.await
|
||||
.map_err(|err| invalid_request(err.to_string()))?;
|
||||
}
|
||||
|
||||
Self::set_app_server_client_info(
|
||||
forked_thread.as_ref(),
|
||||
app_server_client_name,
|
||||
@@ -5395,9 +5468,9 @@ impl ThreadRequestProcessor {
|
||||
let response = ThreadForkResponse {
|
||||
thread: thread.clone(),
|
||||
disabled_plugin_ids: config_snapshot.disabled_plugin_ids,
|
||||
model: session_configured.model,
|
||||
model: config_snapshot.model.clone(),
|
||||
model_provider: session_configured.model_provider_id,
|
||||
service_tier: session_configured.service_tier,
|
||||
service_tier: config_snapshot.service_tier.clone(),
|
||||
cwd: session_configured.cwd,
|
||||
runtime_workspace_roots: config_snapshot.workspace_roots,
|
||||
instruction_sources,
|
||||
@@ -5405,7 +5478,7 @@ impl ThreadRequestProcessor {
|
||||
approvals_reviewer: session_configured.approvals_reviewer.into(),
|
||||
sandbox,
|
||||
active_permission_profile,
|
||||
reasoning_effort: session_configured.reasoning_effort,
|
||||
reasoning_effort: config_snapshot.reasoning_effort.clone(),
|
||||
multi_agent_mode: MultiAgentMode::ExplicitRequestOnly,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user