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 authored and copyberry committed 2026-07-15 21:25:24 +00:00
1 parent cb79b3cdf9
commit 2be648ba4a
36 files changed
+954 -73

No files matched your search

+73
View File
@@ -388,6 +388,7 @@ mod tests {
use std::sync::Arc;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::items::TurnItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::models::BaseInstructions;
@@ -395,13 +396,16 @@ mod tests {
use codex_protocol::models::MessagePhase;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AgentMessageEvent;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ItemCompletedEvent;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::ThreadMemoryMode;
use codex_protocol::protocol::TurnCompleteEvent;
use codex_protocol::protocol::TurnContextItem;
use codex_protocol::protocol::TurnStartedEvent;
use codex_protocol::protocol::UserMessageEvent;
use tempfile::TempDir;
@@ -535,6 +539,74 @@ mod tests {
assert_eq!(metadata.title, "observed append");
}
#[tokio::test]
async fn live_thread_does_not_derive_metadata_from_inherited_items() {
let home = TempDir::new().expect("temp dir");
let config = test_config(home.path());
let runtime = codex_state::StateRuntime::init(
config.sqlite_home.clone(),
config.default_model_provider_id.clone(),
)
.await
.expect("state db should initialize");
let store = Arc::new(LocalThreadStore::new(config, Some(runtime.clone())));
let thread_id = ThreadId::default();
let mut params = create_thread_params(thread_id);
params.history_mode = ThreadHistoryMode::Paginated;
let cwd = std::env::current_dir().expect("current directory");
let turn_context = |model: &str, approval_policy| {
RolloutItem::TurnContext(TurnContextItem {
turn_id: Some("turn-1".to_string()),
cwd: serde_json::from_value(serde_json::json!(cwd)).expect("absolute cwd"),
workspace_roots: None,
current_date: None,
timezone: None,
approval_policy,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: model.to_string(),
comp_hash: None,
personality: None,
collaboration_mode: None,
multi_agent_version: None,
multi_agent_mode: None,
realtime_active: None,
effort: None,
summary: ReasoningSummary::Auto,
})
};
let live_thread = LiveThread::create_with_inherited_model_context(
store,
params,
&[turn_context("parent-model", AskForApproval::Never)],
)
.await
.expect("create live thread with inherited context");
live_thread.persist().await.expect("persist thread");
let inherited_metadata = runtime
.get_thread(thread_id)
.await
.expect("sqlite metadata read")
.expect("sqlite metadata");
assert_eq!(inherited_metadata.model, None);
live_thread
.append_items(&[turn_context("child-model", AskForApproval::OnRequest)])
.await
.expect("append child context");
let child_metadata = runtime
.get_thread(thread_id)
.await
.expect("sqlite metadata read")
.expect("sqlite metadata");
assert_eq!(child_metadata.model.as_deref(), Some("child-model"));
assert_eq!(child_metadata.approval_mode, "on-request");
}
#[tokio::test]
async fn live_thread_output_advances_updated_at_but_not_recency_at() {
let home = TempDir::new().expect("temp dir");
@@ -1393,6 +1465,7 @@ mod tests {
selected_capability_roots: Vec::new(),
multi_agent_version: None,
history_mode: ThreadHistoryMode::Legacy,
subagent_history_start_ordinal: None,
initial_window_id: uuid::Uuid::now_v7().to_string(),
metadata: thread_metadata(),
}