Support metadata staging for reserved thread IDs (#38819)

## Why

Callers need to associate host-owned state with a thread before Core starts it.

## What changed

- Add `ThreadManager::reserve_thread_id` and allow new threads to start with the reserved ID. Reject reserved IDs when resuming an existing thread.
- Let thread stores stage metadata for a reserved ID. The local store merges staged values into the first successful metadata update and then removes them.
- Clear staged metadata when an unmaterialized thread shuts down or is discarded, and require the state database for staging.

## Testing

- Cover reserved ID generation and resume validation.
- Cover metadata persistence, merging, rollout-compatible updates, cleanup, and invalid staging requests.

GitOrigin-RevId: 3f9bc48f0f7764011cc90ea01349e497dda0a9e3
This commit is contained in:
Anton Panasenko
2026-08-16 00:57:28 +00:00
committed by copyberry
parent 5ba12929f8
commit fcdae21073
17 changed files with 650 additions and 21 deletions
+3
View File
@@ -422,6 +422,7 @@ pub(crate) struct SessionSpawnArgs {
pub(crate) environment_selections: Vec<TurnEnvironmentSelection>,
pub(crate) thread_extension_init: ExtensionDataInit,
pub(crate) client_mcp_extensions: ClientMcpExtensions,
pub(crate) reserved_thread_id: Option<ThreadId>,
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
pub(crate) thread_store: Arc<dyn ThreadStore>,
pub(crate) attestation_provider: Option<Arc<dyn AttestationProvider>>,
@@ -514,6 +515,7 @@ impl Session {
environment_selections,
thread_extension_init,
client_mcp_extensions,
reserved_thread_id,
analytics_events_client,
thread_store,
attestation_provider,
@@ -742,6 +744,7 @@ impl Session {
thread_extension_init,
client_mcp_extensions,
agent_control,
reserved_thread_id,
environment_manager,
inherited_environments,
analytics_events_client,
+13 -3
View File
@@ -605,6 +605,7 @@ impl Session {
mut thread_extension_init: ExtensionDataInit,
client_mcp_extensions: ClientMcpExtensions,
agent_control: AgentControl,
reserved_thread_id: Option<ThreadId>,
environment_manager: Arc<EnvironmentManager>,
inherited_environments: Option<TurnEnvironmentSnapshot>,
analytics_events_client: Option<AnalyticsEventsClient>,
@@ -663,11 +664,20 @@ impl Session {
let multi_agent_version = multi_agent_version.map(OnceLock::from).unwrap_or_default();
let initial_multi_agent_version = multi_agent_version.get().copied();
let thread_id = match &initial_history {
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => {
let thread_id = match (&initial_history, reserved_thread_id) {
(
InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_),
Some(thread_id),
) => thread_id,
(InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_), None) => {
agent_control.generate_thread_id()
}
InitialHistory::Resumed(resumed_history) => resumed_history.conversation_id,
(InitialHistory::Resumed(resumed_history), None) => resumed_history.conversation_id,
(InitialHistory::Resumed(_), Some(_)) => {
return Err(anyhow::anyhow!(
"reserved thread ID cannot be used when resuming a thread"
));
}
};
let resumed_session_id = match &initial_history {
InitialHistory::Resumed(resumed) => {
+3
View File
@@ -5695,6 +5695,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
codex_extension_api::ExtensionDataInit::default(),
ClientMcpExtensions::default(),
AgentControl::default(),
/*reserved_thread_id*/ None,
environment_manager,
/*inherited_environments*/ None,
/*analytics_events_client*/ None,
@@ -6122,6 +6123,7 @@ async fn make_session_with_config_and_rx(
codex_extension_api::ExtensionDataInit::default(),
ClientMcpExtensions::default(),
AgentControl::default(),
/*reserved_thread_id*/ None,
environment_manager,
/*inherited_environments*/ None,
/*analytics_events_client*/ None,
@@ -6240,6 +6242,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
codex_extension_api::ExtensionDataInit::default(),
ClientMcpExtensions::default(),
agent_control,
/*reserved_thread_id*/ None,
environment_manager,
/*inherited_environments*/ None,
/*analytics_events_client*/ None,
@@ -832,6 +832,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
environment_selections: Vec::new(),
thread_extension_init: codex_extension_api::ExtensionDataInit::default(),
client_mcp_extensions: ClientMcpExtensions::default(),
reserved_thread_id: None,
analytics_events_client: None,
thread_store,
attestation_provider: None,