Preserve thread settings for goal-first and forked threads (#34469)

## Why

Goal-first and forked threads could create rollouts without recording their
effective thread settings. Resuming those threads after a restart could then
use current configuration instead of the settings selected when the thread was
created.

## What changed

- Persist a `thread_settings_applied` item when a goal first materializes a
  rollout, including a follow-up snapshot if settings changed concurrently.
- Append each fork's effective settings after its copied history so they take
  precedence over inherited settings during resume.
- Centralize conversion from `ThreadConfigSnapshot` to
  `ThreadSettingsSnapshot`.

## Testing

Added coverage that restarts the app server and verifies goal-first and forked
threads retain their respective `approvals_reviewer` settings.

GitOrigin-RevId: 6cb9aaae73bbe9ca432831cff7e826f95ffd6d01
This commit is contained in:
Dylan Hurd authored and copyberry committed 2026-07-21 05:19:49 +00:00
1 parent 9970cd706f
commit 1836ae0612
7 files changed
+230 -30

No files matched your search

+19
View File
@@ -34,6 +34,7 @@ use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::Submission;
use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::ThreadMemoryMode;
use codex_protocol::protocol::ThreadSettingsSnapshot;
use codex_protocol::protocol::ThreadSource;
use codex_protocol::protocol::TokenUsageInfo;
use codex_protocol::protocol::TurnEnvironmentSelection;
@@ -138,6 +139,24 @@ impl ThreadConfigSnapshot {
self.cwd().as_path(),
)
}
pub fn into_thread_settings_snapshot(self) -> ThreadSettingsSnapshot {
let cwd = self.cwd().clone();
ThreadSettingsSnapshot {
model: self.model,
model_provider_id: self.model_provider_id,
service_tier: self.service_tier,
approval_policy: self.approval_policy,
approvals_reviewer: self.approvals_reviewer,
permission_profile: self.permission_profile,
active_permission_profile: self.active_permission_profile,
cwd,
reasoning_effort: self.reasoning_effort,
reasoning_summary: self.reasoning_summary,
personality: self.personality,
collaboration_mode: self.collaboration_mode,
}
}
}
/// Thread settings overrides that app-server validates before starting a turn.
+2 -17
View File
@@ -43,7 +43,6 @@ use codex_protocol::protocol::ThreadMemoryMode;
use codex_protocol::protocol::ThreadRolledBackEvent;
use codex_protocol::protocol::ThreadSettingsAppliedEvent;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::ThreadSettingsSnapshot;
use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::request_permissions::RequestPermissionsResponse;
@@ -165,27 +164,13 @@ async fn thread_settings_update(
}
}
async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
pub(super) async fn thread_settings_applied_event(sess: &Session) -> EventMsg {
let snapshot = {
let state = sess.state.lock().await;
state.session_configuration.thread_config_snapshot()
};
let cwd = snapshot.cwd().clone();
EventMsg::ThreadSettingsApplied(ThreadSettingsAppliedEvent {
thread_settings: ThreadSettingsSnapshot {
model: snapshot.model,
model_provider_id: snapshot.model_provider_id,
service_tier: snapshot.service_tier,
approval_policy: snapshot.approval_policy,
approvals_reviewer: snapshot.approvals_reviewer,
permission_profile: snapshot.permission_profile,
active_permission_profile: snapshot.active_permission_profile,
cwd,
reasoning_effort: snapshot.reasoning_effort,
reasoning_summary: snapshot.reasoning_summary,
personality: snapshot.personality,
collaboration_mode: snapshot.collaboration_mode,
},
thread_settings: snapshot.into_thread_settings_snapshot(),
})
}
+10 -3
View File
@@ -1336,9 +1336,16 @@ impl Session {
state.set_token_info(Some(info));
}
// Paginated subagents persist inherited model context while creating the live
// thread so the copied prefix is not observed as child-owned metadata.
if !rollout_items.is_empty() && !is_paginated_subagent {
let thread_settings_applied =
RolloutItem::EventMsg(handlers::thread_settings_applied_event(self).await);
if is_paginated_subagent {
// Paginated subagents persist inherited model context while creating the live
// thread so the copied prefix is not observed as child-owned metadata.
self.persist_rollout_items(&[thread_settings_applied]).await;
} else {
// Keep the copied prefix and the child's effective settings in one append so a
// cold resume cannot observe inherited settings as the child's latest value.
rollout_items.push(thread_settings_applied);
self.persist_rollout_items(&rollout_items).await;
}