Preserve paginated history for spawned subagents (#33432)

## What changed

- Inherit paginated history mode when spawning or forking a subagent from a paginated parent.
- Load the parent's model context when forking and persist it as an inherited rollout prefix, while excluding that prefix from the child's projected turns, items, and metadata.
- Record the boundary between inherited context and child history, and reject partially initialized paginated subagent rollouts on resume.
- Resume paginated subagents from their stored model context instead of legacy rollout history.

## Testing

Added coverage for fresh and forked paginated subagents, compacted-history forks, model-context resume, inherited-prefix projection, metadata isolation, and incomplete-prefix detection.

GitOrigin-RevId: e57dc37c29aa4aaaf57b052b991be591d730e5ba
This commit is contained in:
Owen Lin
2026-07-15 21:25:24 +00:00
committed by copyberry
parent cb79b3cdf9
commit 2be648ba4a
36 changed files with 954 additions and 73 deletions
+24 -12
View File
@@ -1242,12 +1242,19 @@ impl Session {
}
async fn record_initial_history(&self, conversation_history: InitialHistory) {
let is_subagent = {
let (is_subagent, is_paginated_subagent) = {
let state = self.state.lock().await;
state
.session_configuration
.session_source
.is_non_root_agent()
let session_configuration = &state.session_configuration;
(
session_configuration.session_source.is_non_root_agent(),
matches!(
session_configuration.history_mode,
ThreadHistoryMode::Paginated
) && matches!(
session_configuration.thread_source.as_ref(),
Some(ThreadSource::Subagent)
),
)
};
let has_prior_user_turns = initial_history_has_prior_user_turns(&conversation_history);
{
@@ -1304,11 +1311,7 @@ impl Session {
InitialHistory::Forked(mut rollout_items) => {
let turn_context = self.new_default_turn().await;
if turn_context.item_ids_enabled() {
for rollout_item in &mut rollout_items {
if let RolloutItem::ResponseItem(response_item) = rollout_item {
Self::assign_missing_response_item_id(response_item);
}
}
Self::assign_missing_rollout_response_item_ids(&mut rollout_items);
}
self.apply_rollout_reconstruction(&turn_context, &rollout_items)
.await;
@@ -1320,8 +1323,9 @@ impl Session {
state.set_token_info(Some(info));
}
// If persisting, persist all rollout items as-is (the store filters).
if !rollout_items.is_empty() {
// 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 {
self.persist_rollout_items(&rollout_items).await;
}
@@ -2772,6 +2776,14 @@ impl Session {
item.set_id(Some(ResponseItemId::new(prefix)));
}
fn assign_missing_rollout_response_item_ids(items: &mut [RolloutItem]) {
for item in items {
if let RolloutItem::ResponseItem(response_item) = item {
Self::assign_missing_response_item_id(response_item);
}
}
}
pub(crate) fn response_item_from_user_input(&self, input: Vec<UserInput>) -> ResponseItem {
ResponseItem::from(ResponseInputItem::from_user_input(
input,
+30 -2
View File
@@ -483,7 +483,7 @@ impl Session {
exec_policy: Arc<ExecPolicyManager>,
tx_event: Sender<Event>,
agent_status: watch::Sender<AgentStatus>,
initial_history: InitialHistory,
mut initial_history: InitialHistory,
session_source: SessionSource,
skills_service: Arc<SkillsService>,
plugins_manager: Arc<PluginsManager>,
@@ -515,6 +515,22 @@ impl Session {
.parent_thread_id
.or_else(|| initial_history.get_resumed_parent_thread_id());
session_configuration.parent_thread_id = parent_thread_id;
let is_paginated_subagent = matches!(
session_configuration.history_mode,
ThreadHistoryMode::Paginated
) && matches!(
session_configuration.thread_source.as_ref(),
Some(ThreadSource::Subagent)
);
if (config.features.enabled(Feature::ItemIds)
|| matches!(
session_configuration.history_mode,
ThreadHistoryMode::Paginated
))
&& let InitialHistory::Forked(items) = &mut initial_history
{
Self::assign_missing_rollout_response_item_ids(items);
}
let multi_agent_version = multi_agent_version.map(OnceLock::from).unwrap_or_default();
let initial_multi_agent_version = multi_agent_version.get().copied();
@@ -599,6 +615,7 @@ impl Session {
selected_capability_roots: selected_capability_roots.clone(),
multi_agent_version: initial_multi_agent_version,
history_mode: session_configuration.history_mode,
subagent_history_start_ordinal: None,
initial_window_id: initial_auto_compact_window_ids
.window_id
.to_string(),
@@ -612,7 +629,18 @@ impl Session {
},
},
};
LiveThread::create(Arc::clone(&thread_store), params).await?
if is_paginated_subagent
&& let InitialHistory::Forked(items) = &initial_history
{
LiveThread::create_with_inherited_model_context(
Arc::clone(&thread_store),
params,
items,
)
.await?
} else {
LiveThread::create(Arc::clone(&thread_store), params).await?
}
}
InitialHistory::Resumed(resumed_history) => {
let params = ResumeThreadParams {
+4
View File
@@ -4142,6 +4142,7 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
selected_capability_roots: Vec::new(),
multi_agent_version: None,
history_mode: Default::default(),
subagent_history_start_ordinal: None,
initial_window_id: Uuid::now_v7().to_string(),
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),
@@ -6903,6 +6904,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
selected_capability_roots: Vec::new(),
multi_agent_version: None,
history_mode: Default::default(),
subagent_history_start_ordinal: None,
initial_window_id: Uuid::now_v7().to_string(),
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),
@@ -6980,6 +6982,7 @@ async fn submission_loop_channel_close_runs_full_thread_teardown() {
selected_capability_roots: Vec::new(),
multi_agent_version: None,
history_mode: Default::default(),
subagent_history_start_ordinal: None,
initial_window_id: Uuid::now_v7().to_string(),
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),
@@ -9210,6 +9213,7 @@ async fn attach_in_memory_thread_store(
selected_capability_roots: Vec::new(),
multi_agent_version: None,
history_mode: Default::default(),
subagent_history_start_ordinal: None,
initial_window_id: Uuid::now_v7().to_string(),
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),