mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
codex: persist multi-agent version metadata
This commit is contained in:
@@ -2175,6 +2175,7 @@ mod tests {
|
||||
model_provider: "openai".to_string(),
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
multi_agent_version: None,
|
||||
created_at,
|
||||
updated_at: created_at,
|
||||
archived_at: None,
|
||||
|
||||
@@ -400,6 +400,7 @@ mod thread_processor_behavior_tests {
|
||||
model_provider: "openai".to_string(),
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
multi_agent_version: None,
|
||||
created_at: created_at.with_timezone(&Utc),
|
||||
updated_at: updated_at.with_timezone(&Utc),
|
||||
archived_at: None,
|
||||
|
||||
@@ -143,6 +143,7 @@ pub fn create_fake_rollout_with_source(
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
@@ -227,6 +228,7 @@ pub fn create_fake_rollout_with_text_elements(
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: model_provider.map(str::to_string),
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
|
||||
@@ -131,6 +131,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() ->
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: ThreadMemoryMode::Disabled,
|
||||
},
|
||||
event_persistence_mode: ThreadEventPersistenceMode::default(),
|
||||
|
||||
@@ -1365,6 +1365,7 @@ async fn seed_pathless_store_thread(
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: ThreadMemoryMode::Disabled,
|
||||
},
|
||||
event_persistence_mode: ThreadEventPersistenceMode::default(),
|
||||
|
||||
@@ -1749,6 +1749,7 @@ stream_max_retries = 0
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("mock_provider".to_string()),
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
|
||||
@@ -218,6 +218,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: ThreadMemoryMode::Disabled,
|
||||
},
|
||||
event_persistence_mode: ThreadEventPersistenceMode::default(),
|
||||
|
||||
@@ -55,6 +55,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
|
||||
@@ -37,6 +37,7 @@ fn stored_thread(cwd: &str, title: &str, first_user_message: &str) -> StoredThre
|
||||
model_provider: "test-provider".to_string(),
|
||||
model: Some("gpt-5.2".to_string()),
|
||||
reasoning_effort: None,
|
||||
multi_agent_version: None,
|
||||
created_at: Utc
|
||||
.timestamp_opt(1_709_251_100, 0)
|
||||
.single()
|
||||
|
||||
@@ -559,6 +559,7 @@ impl Session {
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: Some(config.cwd.to_path_buf()),
|
||||
model_provider: config.model_provider_id.clone(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: if config.memories.generate_memories {
|
||||
ThreadMemoryMode::Enabled
|
||||
} else {
|
||||
@@ -581,6 +582,7 @@ impl Session {
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: Some(config.cwd.to_path_buf()),
|
||||
model_provider: config.model_provider_id.clone(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: if config.memories.generate_memories {
|
||||
ThreadMemoryMode::Enabled
|
||||
} else {
|
||||
|
||||
@@ -3456,6 +3456,7 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: Some(config.cwd.to_path_buf()),
|
||||
model_provider: config.model_provider_id.clone(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: if config.memories.generate_memories {
|
||||
ThreadMemoryMode::Enabled
|
||||
} else {
|
||||
@@ -5951,6 +5952,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: Some(config.cwd.to_path_buf()),
|
||||
model_provider: config.model_provider_id.clone(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: if config.memories.generate_memories {
|
||||
ThreadMemoryMode::Enabled
|
||||
} else {
|
||||
|
||||
@@ -71,6 +71,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
@@ -119,6 +120,7 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
|
||||
@@ -186,6 +186,7 @@ async fn find_locates_rollout_file_written_by_recorder() -> std::io::Result<()>
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
/*thread_source*/ None,
|
||||
/*multi_agent_version*/ None,
|
||||
BaseInstructions::default(),
|
||||
Vec::new(),
|
||||
),
|
||||
|
||||
@@ -222,6 +222,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
|
||||
@@ -237,6 +237,13 @@ pub enum ToolMode {
|
||||
CodeModeOnly,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MultiAgentVersion {
|
||||
V1,
|
||||
V2,
|
||||
}
|
||||
|
||||
fn deserialize_optional_model_selector<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
|
||||
@@ -43,6 +43,7 @@ use crate::models::ResponseItem;
|
||||
use crate::models::SandboxEnforcement;
|
||||
use crate::models::WebSearchAction;
|
||||
use crate::num_format::format_with_separators;
|
||||
use crate::openai_models::MultiAgentVersion;
|
||||
use crate::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use crate::parse_command::ParsedCommand;
|
||||
use crate::plan_tool::UpdatePlanArgs;
|
||||
@@ -53,6 +54,7 @@ use crate::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use serde_with::serde_as;
|
||||
@@ -2461,6 +2463,33 @@ impl InitialHistory {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_multi_agent_version(&self) -> Option<MultiAgentVersion> {
|
||||
match self {
|
||||
InitialHistory::New | InitialHistory::Cleared => None,
|
||||
InitialHistory::Resumed(resumed) => resumed
|
||||
.history
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line)
|
||||
if meta_line.meta.id == resumed.conversation_id =>
|
||||
{
|
||||
Some(meta_line.meta.multi_agent_version)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.flatten(),
|
||||
InitialHistory::Forked(items) => items
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.multi_agent_version),
|
||||
_ => None,
|
||||
})
|
||||
.flatten(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn session_cwd_from_items(items: &[RolloutItem]) -> Option<PathBuf> {
|
||||
@@ -2734,6 +2763,13 @@ pub struct SessionMeta {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent_path: Option<String>,
|
||||
pub model_provider: Option<String>,
|
||||
/// Thread-scoped multi-agent runtime selector.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_optional_multi_agent_version",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
/// base_instructions for the session. This *should* always be present when creating a new session,
|
||||
/// but may be missing for older sessions. If not present, fall back to rendering the base_instructions
|
||||
/// from ModelsManager.
|
||||
@@ -2744,6 +2780,18 @@ pub struct SessionMeta {
|
||||
pub memory_mode: Option<String>,
|
||||
}
|
||||
|
||||
fn deserialize_optional_multi_agent_version<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<MultiAgentVersion>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let Some(value) = Option::<String>::deserialize(deserializer)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(serde_json::from_value(Value::String(value)).ok())
|
||||
}
|
||||
|
||||
impl Default for SessionMeta {
|
||||
fn default() -> Self {
|
||||
SessionMeta {
|
||||
@@ -2759,6 +2807,7 @@ impl Default for SessionMeta {
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
model_provider: None,
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
@@ -5488,4 +5537,62 @@ mod tests {
|
||||
|
||||
assert_eq!(info.model_context_window, Some(258_400));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_meta_treats_unknown_multi_agent_version_as_omitted() -> Result<()> {
|
||||
let mut value = serde_json::to_value(SessionMeta::default())?;
|
||||
value["multi_agent_version"] = json!("v3");
|
||||
|
||||
let meta: SessionMeta = serde_json::from_value(value)?;
|
||||
|
||||
assert_eq!(meta.multi_agent_version, None);
|
||||
assert_eq!(
|
||||
serde_json::to_value(meta)?
|
||||
.as_object()
|
||||
.expect("session meta should serialize as an object")
|
||||
.get("multi_agent_version"),
|
||||
None
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumed_history_uses_latest_matching_multi_agent_version() {
|
||||
let thread_id = ThreadId::new();
|
||||
let unrelated_thread_id = ThreadId::new();
|
||||
let initial_history = InitialHistory::Resumed(ResumedHistory {
|
||||
conversation_id: thread_id,
|
||||
history: vec![
|
||||
RolloutItem::SessionMeta(SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
..Default::default()
|
||||
},
|
||||
git: None,
|
||||
}),
|
||||
RolloutItem::SessionMeta(SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
id: unrelated_thread_id,
|
||||
multi_agent_version: Some(MultiAgentVersion::V1),
|
||||
..Default::default()
|
||||
},
|
||||
git: None,
|
||||
}),
|
||||
RolloutItem::SessionMeta(SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
multi_agent_version: Some(MultiAgentVersion::V2),
|
||||
..Default::default()
|
||||
},
|
||||
git: None,
|
||||
}),
|
||||
],
|
||||
rollout_path: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
initial_history.get_multi_agent_version(),
|
||||
Some(MultiAgentVersion::V2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::protocol::EventMsg;
|
||||
use crate::state_db;
|
||||
use codex_file_search as file_search;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::openai_models::MultiAgentVersion;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
@@ -68,6 +69,8 @@ pub struct ThreadItem {
|
||||
pub agent_role: Option<String>,
|
||||
/// Model provider from session metadata.
|
||||
pub model_provider: Option<String>,
|
||||
/// Thread-scoped multi-agent runtime selector.
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
/// CLI version from session metadata.
|
||||
pub cli_version: Option<String>,
|
||||
/// RFC3339 timestamp string for when the session was created, if available.
|
||||
@@ -98,6 +101,7 @@ struct HeadTailSummary {
|
||||
agent_nickname: Option<String>,
|
||||
agent_role: Option<String>,
|
||||
model_provider: Option<String>,
|
||||
multi_agent_version: Option<MultiAgentVersion>,
|
||||
cli_version: Option<String>,
|
||||
created_at: Option<String>,
|
||||
updated_at: Option<String>,
|
||||
@@ -781,6 +785,7 @@ async fn build_thread_item(
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
multi_agent_version,
|
||||
cli_version,
|
||||
created_at,
|
||||
updated_at: mut summary_updated_at,
|
||||
@@ -802,6 +807,7 @@ async fn build_thread_item(
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
multi_agent_version,
|
||||
cli_version,
|
||||
created_at,
|
||||
updated_at: summary_updated_at,
|
||||
@@ -1104,6 +1110,7 @@ async fn read_head_summary(path: &Path, head_limit: usize) -> io::Result<HeadTai
|
||||
summary.agent_nickname = session_meta_line.meta.agent_nickname.clone();
|
||||
summary.agent_role = session_meta_line.meta.agent_role.clone();
|
||||
summary.model_provider = session_meta_line.meta.model_provider.clone();
|
||||
summary.multi_agent_version = session_meta_line.meta.multi_agent_version;
|
||||
summary.thread_id = Some(session_meta_line.meta.id);
|
||||
summary.cwd = Some(session_meta_line.meta.cwd.clone());
|
||||
summary.git_branch = session_meta_line
|
||||
|
||||
@@ -47,6 +47,7 @@ pub(crate) fn builder_from_session_meta(
|
||||
session_meta.meta.source.clone(),
|
||||
);
|
||||
builder.model_provider = session_meta.meta.model_provider.clone();
|
||||
builder.multi_agent_version = session_meta.meta.multi_agent_version;
|
||||
builder.agent_nickname = session_meta.meta.agent_nickname.clone();
|
||||
builder.agent_role = session_meta.meta.agent_role.clone();
|
||||
builder.agent_path = session_meta.meta.agent_path.clone();
|
||||
|
||||
@@ -45,6 +45,7 @@ async fn extract_metadata_from_rollout_uses_session_meta() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
@@ -97,6 +98,7 @@ async fn extract_metadata_from_rollout_returns_latest_memory_mode() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
@@ -357,6 +359,7 @@ fn write_rollout_in_sessions_with_cwd(
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("test-provider".to_string()),
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
|
||||
@@ -13,6 +13,7 @@ use chrono::SecondsFormat;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::openai_models::MultiAgentVersion;
|
||||
use serde_json::Value;
|
||||
use time::OffsetDateTime;
|
||||
use time::format_description::FormatItem;
|
||||
@@ -83,6 +84,7 @@ pub enum RolloutRecorderParams {
|
||||
forked_from_id: Option<ThreadId>,
|
||||
source: SessionSource,
|
||||
thread_source: Option<ThreadSource>,
|
||||
multi_agent_version: Option<MultiAgentVersion>,
|
||||
base_instructions: BaseInstructions,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
},
|
||||
@@ -158,6 +160,7 @@ impl RolloutRecorderParams {
|
||||
forked_from_id: Option<ThreadId>,
|
||||
source: SessionSource,
|
||||
thread_source: Option<ThreadSource>,
|
||||
multi_agent_version: Option<MultiAgentVersion>,
|
||||
base_instructions: BaseInstructions,
|
||||
dynamic_tools: Vec<DynamicToolSpec>,
|
||||
) -> Self {
|
||||
@@ -166,6 +169,7 @@ impl RolloutRecorderParams {
|
||||
forked_from_id,
|
||||
source,
|
||||
thread_source,
|
||||
multi_agent_version,
|
||||
base_instructions,
|
||||
dynamic_tools,
|
||||
}
|
||||
@@ -654,6 +658,7 @@ impl RolloutRecorder {
|
||||
forked_from_id,
|
||||
source,
|
||||
thread_source,
|
||||
multi_agent_version,
|
||||
base_instructions,
|
||||
dynamic_tools,
|
||||
} => {
|
||||
@@ -683,6 +688,7 @@ impl RolloutRecorder {
|
||||
source,
|
||||
thread_source,
|
||||
model_provider: Some(config.model_provider_id().to_string()),
|
||||
multi_agent_version,
|
||||
base_instructions: Some(base_instructions),
|
||||
dynamic_tools: if dynamic_tools.is_empty() {
|
||||
None
|
||||
@@ -1023,6 +1029,7 @@ fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadIt
|
||||
agent_nickname,
|
||||
agent_role,
|
||||
model_provider,
|
||||
multi_agent_version,
|
||||
cli_version,
|
||||
created_at,
|
||||
updated_at,
|
||||
@@ -1058,6 +1065,9 @@ fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadIt
|
||||
if item.model_provider.is_none() {
|
||||
item.model_provider = model_provider;
|
||||
}
|
||||
if item.multi_agent_version.is_none() {
|
||||
item.multi_agent_version = multi_agent_version;
|
||||
}
|
||||
if item.cli_version.is_none() {
|
||||
item.cli_version = cli_version;
|
||||
}
|
||||
@@ -1693,6 +1703,7 @@ fn thread_item_from_state_metadata(item: codex_state::ThreadMetadata) -> ThreadI
|
||||
agent_nickname: item.agent_nickname,
|
||||
agent_role: item.agent_role,
|
||||
model_provider: Some(item.model_provider),
|
||||
multi_agent_version: item.multi_agent_version,
|
||||
cli_version: Some(item.cli_version),
|
||||
created_at: Some(item.created_at.to_rfc3339_opts(SecondsFormat::Secs, true)),
|
||||
updated_at: Some(item.updated_at.to_rfc3339_opts(SecondsFormat::Millis, true)),
|
||||
|
||||
@@ -95,6 +95,7 @@ async fn state_db_init_backfills_before_returning() -> anyhow::Result<()> {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
@@ -371,6 +372,7 @@ async fn recorder_materializes_on_flush_with_pending_items() -> std::io::Result<
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
/*thread_source*/ None,
|
||||
/*multi_agent_version*/ None,
|
||||
BaseInstructions::default(),
|
||||
Vec::new(),
|
||||
),
|
||||
@@ -451,6 +453,7 @@ async fn persist_reports_filesystem_error_and_retries_buffered_items() -> std::i
|
||||
/*forked_from_id*/ None,
|
||||
SessionSource::Exec,
|
||||
/*thread_source*/ None,
|
||||
/*multi_agent_version*/ None,
|
||||
BaseInstructions::default(),
|
||||
Vec::new(),
|
||||
),
|
||||
@@ -977,6 +980,7 @@ fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fi
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
multi_agent_version: None,
|
||||
cli_version: None,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
@@ -994,6 +998,7 @@ fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fi
|
||||
agent_nickname: Some("state-agent".to_string()),
|
||||
agent_role: Some("state-role".to_string()),
|
||||
model_provider: Some("state-provider".to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("state-version".to_string()),
|
||||
created_at: Some("2025-01-03T16:00:00Z".to_string()),
|
||||
updated_at: Some("2025-01-03T16:01:02.003Z".to_string()),
|
||||
|
||||
@@ -37,6 +37,7 @@ fn write_rollout_with_metadata(path: &Path, thread_id: ThreadId) -> std::io::Res
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("test-provider".into()),
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
|
||||
@@ -609,6 +609,7 @@ async fn test_list_conversations_latest_first() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-01-03T12-00-00".into()),
|
||||
updated_at: updated_times.first().cloned().flatten(),
|
||||
@@ -626,6 +627,7 @@ async fn test_list_conversations_latest_first() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-01-02T12-00-00".into()),
|
||||
updated_at: updated_times.get(1).cloned().flatten(),
|
||||
@@ -643,6 +645,7 @@ async fn test_list_conversations_latest_first() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-01-01T12-00-00".into()),
|
||||
updated_at: updated_times.get(2).cloned().flatten(),
|
||||
@@ -753,6 +756,7 @@ async fn test_pagination_cursor() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-05T09-00-00".into()),
|
||||
updated_at: updated_page1.first().cloned().flatten(),
|
||||
@@ -770,6 +774,7 @@ async fn test_pagination_cursor() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-04T09-00-00".into()),
|
||||
updated_at: updated_page1.get(1).cloned().flatten(),
|
||||
@@ -823,6 +828,7 @@ async fn test_pagination_cursor() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-03T09-00-00".into()),
|
||||
updated_at: updated_page2.first().cloned().flatten(),
|
||||
@@ -840,6 +846,7 @@ async fn test_pagination_cursor() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-02T09-00-00".into()),
|
||||
updated_at: updated_page2.get(1).cloned().flatten(),
|
||||
@@ -885,6 +892,7 @@ async fn test_pagination_cursor() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some("2025-03-01T09-00-00".into()),
|
||||
updated_at: updated_page3.first().cloned().flatten(),
|
||||
@@ -1055,6 +1063,7 @@ async fn test_get_thread_contents() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some(ts.into()),
|
||||
updated_at: page.items[0].updated_at.clone(),
|
||||
@@ -1261,6 +1270,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("test-provider".into()),
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
@@ -1406,6 +1416,7 @@ async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some(ts.to_string()),
|
||||
updated_at: updated_page1.first().cloned().flatten(),
|
||||
@@ -1423,6 +1434,7 @@ async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties() {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some(TEST_PROVIDER.to_string()),
|
||||
multi_agent_version: None,
|
||||
cli_version: Some("test_version".to_string()),
|
||||
created_at: Some(ts.to_string()),
|
||||
updated_at: updated_page1.get(1).cloned().flatten(),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE threads ADD COLUMN multi_agent_version TEXT;
|
||||
@@ -54,6 +54,7 @@ fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &Sessi
|
||||
metadata.agent_nickname = meta_line.meta.agent_nickname.clone();
|
||||
metadata.agent_role = meta_line.meta.agent_role.clone();
|
||||
metadata.agent_path = meta_line.meta.agent_path.clone();
|
||||
metadata.multi_agent_version = meta_line.meta.multi_agent_version;
|
||||
if let Some(provider) = meta_line.meta.model_provider.as_deref() {
|
||||
metadata.model_provider = provider.to_string();
|
||||
}
|
||||
@@ -332,6 +333,7 @@ mod tests {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
@@ -489,6 +491,7 @@ mod tests {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: Some("openai".to_string()),
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
@@ -518,6 +521,7 @@ mod tests {
|
||||
model_provider: "openai".to_string(),
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
multi_agent_version: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
title: String::new(),
|
||||
|
||||
@@ -2,11 +2,13 @@ use anyhow::Result;
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::openai_models::MultiAgentVersion;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use serde_json::Value;
|
||||
use sqlx::Row;
|
||||
use sqlx::sqlite::SqliteRow;
|
||||
use std::path::PathBuf;
|
||||
@@ -83,6 +85,8 @@ pub struct ThreadMetadata {
|
||||
pub model: Option<String>,
|
||||
/// The latest observed reasoning effort for the thread.
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
/// Thread-scoped multi-agent runtime selector.
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
/// The working directory for the thread.
|
||||
pub cwd: PathBuf,
|
||||
/// Version of the CLI that created the thread.
|
||||
@@ -132,6 +136,8 @@ pub struct ThreadMetadataBuilder {
|
||||
pub agent_path: Option<String>,
|
||||
/// The model provider identifier, if known.
|
||||
pub model_provider: Option<String>,
|
||||
/// Thread-scoped multi-agent runtime selector.
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
/// The working directory for the thread.
|
||||
pub cwd: PathBuf,
|
||||
/// Version of the CLI that created the thread.
|
||||
@@ -169,6 +175,7 @@ impl ThreadMetadataBuilder {
|
||||
agent_role: None,
|
||||
agent_path: None,
|
||||
model_provider: None,
|
||||
multi_agent_version: None,
|
||||
cwd: PathBuf::new(),
|
||||
cli_version: None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
@@ -209,6 +216,7 @@ impl ThreadMetadataBuilder {
|
||||
.unwrap_or_else(|| default_provider.to_string()),
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
multi_agent_version: self.multi_agent_version,
|
||||
cwd: self.cwd.clone(),
|
||||
cli_version: self.cli_version.clone().unwrap_or_default(),
|
||||
title: String::new(),
|
||||
@@ -275,6 +283,9 @@ impl ThreadMetadata {
|
||||
if self.reasoning_effort != other.reasoning_effort {
|
||||
diffs.push("reasoning_effort");
|
||||
}
|
||||
if self.multi_agent_version != other.multi_agent_version {
|
||||
diffs.push("multi_agent_version");
|
||||
}
|
||||
if self.cwd != other.cwd {
|
||||
diffs.push("cwd");
|
||||
}
|
||||
@@ -333,6 +344,7 @@ pub(crate) struct ThreadRow {
|
||||
model_provider: String,
|
||||
model: Option<String>,
|
||||
reasoning_effort: Option<String>,
|
||||
multi_agent_version: Option<String>,
|
||||
cwd: String,
|
||||
cli_version: String,
|
||||
title: String,
|
||||
@@ -362,6 +374,7 @@ impl ThreadRow {
|
||||
model_provider: row.try_get("model_provider")?,
|
||||
model: row.try_get("model")?,
|
||||
reasoning_effort: row.try_get("reasoning_effort")?,
|
||||
multi_agent_version: row.try_get("multi_agent_version")?,
|
||||
cwd: row.try_get("cwd")?,
|
||||
cli_version: row.try_get("cli_version")?,
|
||||
title: row.try_get("title")?,
|
||||
@@ -395,6 +408,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
||||
model_provider,
|
||||
model,
|
||||
reasoning_effort,
|
||||
multi_agent_version,
|
||||
cwd,
|
||||
cli_version,
|
||||
title,
|
||||
@@ -426,6 +440,8 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
||||
model,
|
||||
reasoning_effort: reasoning_effort
|
||||
.and_then(|value| value.parse::<ReasoningEffort>().ok()),
|
||||
multi_agent_version: multi_agent_version
|
||||
.and_then(|value| serde_json::from_value(Value::String(value)).ok()),
|
||||
cwd: PathBuf::from(cwd),
|
||||
cli_version,
|
||||
title,
|
||||
@@ -512,6 +528,7 @@ mod tests {
|
||||
model_provider: "openai".to_string(),
|
||||
model: Some("gpt-5".to_string()),
|
||||
reasoning_effort: reasoning_effort.map(str::to_string),
|
||||
multi_agent_version: None,
|
||||
cwd: "/tmp/workspace".to_string(),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
title: String::new(),
|
||||
@@ -542,6 +559,7 @@ mod tests {
|
||||
model_provider: "openai".to_string(),
|
||||
model: Some("gpt-5".to_string()),
|
||||
reasoning_effort,
|
||||
multi_agent_version: None,
|
||||
cwd: PathBuf::from("/tmp/workspace"),
|
||||
cli_version: "0.0.0".to_string(),
|
||||
title: String::new(),
|
||||
|
||||
@@ -177,6 +177,7 @@ SELECT
|
||||
threads.agent_nickname,
|
||||
threads.agent_role,
|
||||
threads.model_provider,
|
||||
threads.multi_agent_version,
|
||||
threads.model,
|
||||
threads.reasoning_effort,
|
||||
threads.cwd,
|
||||
@@ -547,6 +548,7 @@ SELECT
|
||||
threads.agent_role,
|
||||
threads.agent_path,
|
||||
threads.model_provider,
|
||||
threads.multi_agent_version,
|
||||
threads.model,
|
||||
threads.reasoning_effort,
|
||||
threads.cwd,
|
||||
|
||||
@@ -55,6 +55,7 @@ pub(super) fn test_thread_metadata(
|
||||
model_provider: "test-provider".to_string(),
|
||||
model: Some("gpt-5".to_string()),
|
||||
reasoning_effort: Some(ReasoningEffort::Medium),
|
||||
multi_agent_version: None,
|
||||
cwd,
|
||||
cli_version: "0.0.0".to_string(),
|
||||
title: String::new(),
|
||||
|
||||
@@ -18,6 +18,7 @@ SELECT
|
||||
threads.agent_role,
|
||||
threads.agent_path,
|
||||
threads.model_provider,
|
||||
threads.multi_agent_version,
|
||||
threads.model,
|
||||
threads.reasoning_effort,
|
||||
threads.cwd,
|
||||
@@ -491,6 +492,7 @@ INSERT INTO threads (
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
multi_agent_version,
|
||||
model,
|
||||
reasoning_effort,
|
||||
cwd,
|
||||
@@ -507,7 +509,7 @@ INSERT INTO threads (
|
||||
git_branch,
|
||||
git_origin_url,
|
||||
memory_mode
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO NOTHING
|
||||
"#,
|
||||
)
|
||||
@@ -527,6 +529,12 @@ ON CONFLICT(id) DO NOTHING
|
||||
.bind(metadata.agent_role.as_deref())
|
||||
.bind(metadata.agent_path.as_deref())
|
||||
.bind(metadata.model_provider.as_str())
|
||||
.bind(
|
||||
metadata
|
||||
.multi_agent_version
|
||||
.as_ref()
|
||||
.map(crate::extract::enum_to_string),
|
||||
)
|
||||
.bind(metadata.model.as_deref())
|
||||
.bind(
|
||||
metadata
|
||||
@@ -697,6 +705,7 @@ INSERT INTO threads (
|
||||
agent_role,
|
||||
agent_path,
|
||||
model_provider,
|
||||
multi_agent_version,
|
||||
model,
|
||||
reasoning_effort,
|
||||
cwd,
|
||||
@@ -713,7 +722,7 @@ INSERT INTO threads (
|
||||
git_branch,
|
||||
git_origin_url,
|
||||
memory_mode
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
rollout_path = excluded.rollout_path,
|
||||
created_at = excluded.created_at,
|
||||
@@ -726,6 +735,7 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
agent_role = excluded.agent_role,
|
||||
agent_path = excluded.agent_path,
|
||||
model_provider = excluded.model_provider,
|
||||
multi_agent_version = excluded.multi_agent_version,
|
||||
model = excluded.model,
|
||||
reasoning_effort = excluded.reasoning_effort,
|
||||
cwd = excluded.cwd,
|
||||
@@ -759,6 +769,12 @@ ON CONFLICT(id) DO UPDATE SET
|
||||
.bind(metadata.agent_role.as_deref())
|
||||
.bind(metadata.agent_path.as_deref())
|
||||
.bind(metadata.model_provider.as_str())
|
||||
.bind(
|
||||
metadata
|
||||
.multi_agent_version
|
||||
.as_ref()
|
||||
.map(crate::extract::enum_to_string),
|
||||
)
|
||||
.bind(metadata.model.as_deref())
|
||||
.bind(
|
||||
metadata
|
||||
@@ -929,6 +945,7 @@ SELECT
|
||||
threads.agent_role,
|
||||
threads.agent_path,
|
||||
threads.model_provider,
|
||||
threads.multi_agent_version,
|
||||
threads.model,
|
||||
threads.reasoning_effort,
|
||||
threads.cwd,
|
||||
@@ -1328,6 +1345,7 @@ mod tests {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: Some("polluted".to_string()),
|
||||
@@ -1387,6 +1405,7 @@ mod tests {
|
||||
agent_nickname: None,
|
||||
agent_role: None,
|
||||
model_provider: None,
|
||||
multi_agent_version: None,
|
||||
base_instructions: None,
|
||||
dynamic_tools: None,
|
||||
memory_mode: None,
|
||||
|
||||
@@ -337,6 +337,9 @@ fn stored_thread_from_state(
|
||||
.unwrap_or_else(|| "test".to_string()),
|
||||
model: metadata.and_then(|metadata| metadata.model.clone()),
|
||||
reasoning_effort: metadata.and_then(|metadata| metadata.reasoning_effort),
|
||||
multi_agent_version: metadata
|
||||
.and_then(|metadata| metadata.multi_agent_version)
|
||||
.unwrap_or(created.metadata.multi_agent_version),
|
||||
created_at: metadata
|
||||
.and_then(|metadata| metadata.created_at)
|
||||
.unwrap_or_else(Utc::now),
|
||||
|
||||
@@ -32,6 +32,7 @@ pub(super) async fn create_thread(
|
||||
params.forked_from_id,
|
||||
params.source,
|
||||
params.thread_source,
|
||||
params.metadata.multi_agent_version,
|
||||
params.base_instructions,
|
||||
params.dynamic_tools,
|
||||
),
|
||||
|
||||
@@ -130,6 +130,7 @@ pub(super) fn stored_thread_from_rollout_item(
|
||||
.unwrap_or_else(|| default_provider.to_string()),
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
multi_agent_version: item.multi_agent_version,
|
||||
created_at,
|
||||
updated_at,
|
||||
archived_at,
|
||||
|
||||
@@ -296,8 +296,11 @@ mod tests {
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::openai_models::MultiAgentVersion;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadMemoryMode;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
@@ -360,6 +363,93 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_thread_persists_multi_agent_version_in_rollout_and_sqlite() {
|
||||
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 live_thread = LiveThread::create(store.clone(), create_thread_params(thread_id))
|
||||
.await
|
||||
.expect("create live thread");
|
||||
|
||||
live_thread
|
||||
.append_items(&[user_message_item("persist selector")])
|
||||
.await
|
||||
.expect("append live item");
|
||||
live_thread
|
||||
.append_items(&[RolloutItem::SessionMeta(SessionMetaLine {
|
||||
meta: SessionMeta {
|
||||
id: thread_id,
|
||||
source: SessionSource::Exec,
|
||||
multi_agent_version: Some(MultiAgentVersion::V2),
|
||||
..Default::default()
|
||||
},
|
||||
git: None,
|
||||
})])
|
||||
.await
|
||||
.expect("append selector update");
|
||||
live_thread.flush().await.expect("flush live thread");
|
||||
let rollout_path = store
|
||||
.live_rollout_path(thread_id)
|
||||
.await
|
||||
.expect("load rollout path");
|
||||
|
||||
let metadata = runtime
|
||||
.get_thread(thread_id)
|
||||
.await
|
||||
.expect("sqlite metadata read")
|
||||
.expect("sqlite metadata");
|
||||
let stored_thread = store
|
||||
.read_thread(ReadThreadParams {
|
||||
thread_id,
|
||||
include_archived: false,
|
||||
include_history: false,
|
||||
})
|
||||
.await
|
||||
.expect("stored thread read");
|
||||
let (items, _, _) = RolloutRecorder::load_rollout_items(rollout_path.as_path())
|
||||
.await
|
||||
.expect("load rollout items");
|
||||
let persisted_versions = items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.multi_agent_version),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let jsonl_only_thread =
|
||||
LocalThreadStore::new(test_config(home.path()), /*state_db*/ None)
|
||||
.read_thread_by_rollout_path(
|
||||
rollout_path,
|
||||
/*include_archived*/ false,
|
||||
/*include_history*/ true,
|
||||
)
|
||||
.await
|
||||
.expect("jsonl-only stored thread read");
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
persisted_versions,
|
||||
metadata.multi_agent_version,
|
||||
stored_thread.multi_agent_version,
|
||||
jsonl_only_thread.multi_agent_version
|
||||
),
|
||||
(
|
||||
vec![None, Some(MultiAgentVersion::V2)],
|
||||
Some(MultiAgentVersion::V2),
|
||||
Some(MultiAgentVersion::V2),
|
||||
Some(MultiAgentVersion::V2)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn raw_append_items_does_not_update_sqlite_metadata() {
|
||||
// This pins the ThreadStore contract: raw appends are history-only. Callers that need
|
||||
@@ -537,6 +627,7 @@ mod tests {
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: Some(home.path().to_path_buf()),
|
||||
model_provider: "different-provider".to_string(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: ThreadMemoryMode::Enabled,
|
||||
},
|
||||
event_persistence_mode: ThreadEventPersistenceMode::Limited,
|
||||
@@ -592,6 +683,7 @@ mod tests {
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: Some(home.path().to_path_buf()),
|
||||
model_provider: "different-provider".to_string(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: ThreadMemoryMode::Enabled,
|
||||
},
|
||||
event_persistence_mode: ThreadEventPersistenceMode::Limited,
|
||||
@@ -806,6 +898,7 @@ mod tests {
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: ThreadMemoryMode::Enabled,
|
||||
},
|
||||
event_persistence_mode: ThreadEventPersistenceMode::Limited,
|
||||
@@ -1036,6 +1129,7 @@ mod tests {
|
||||
ThreadPersistenceMetadata {
|
||||
cwd: Some(std::env::current_dir().expect("cwd")),
|
||||
model_provider: "test-provider".to_string(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: ThreadMemoryMode::Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_rollout::RolloutRecorder;
|
||||
@@ -59,6 +60,7 @@ pub(super) async fn read_thread(
|
||||
rollout_thread.name = thread.name;
|
||||
}
|
||||
rollout_thread.git_info = thread.git_info;
|
||||
rollout_thread.multi_agent_version = thread.multi_agent_version;
|
||||
rollout_thread.permission_profile = permission_profile_from_metadata_value(
|
||||
&metadata_sandbox_policy,
|
||||
rollout_thread.cwd.as_path(),
|
||||
@@ -129,6 +131,9 @@ pub(super) async fn read_thread_by_rollout_path(
|
||||
metadata.git_branch.or(fallback_branch),
|
||||
metadata.git_origin_url.or(fallback_origin_url),
|
||||
);
|
||||
if metadata.multi_agent_version.is_some() {
|
||||
thread.multi_agent_version = metadata.multi_agent_version;
|
||||
}
|
||||
}
|
||||
attach_history_if_requested(&mut thread, include_history).await?;
|
||||
Ok(thread)
|
||||
@@ -162,6 +167,14 @@ async fn attach_history_if_requested(
|
||||
});
|
||||
};
|
||||
let items = load_history_items(&path).await?;
|
||||
if let Some(multi_agent_version) = items.iter().rev().find_map(|item| match item {
|
||||
RolloutItem::SessionMeta(meta_line) if meta_line.meta.id == thread_id => {
|
||||
Some(meta_line.meta.multi_agent_version)
|
||||
}
|
||||
_ => None,
|
||||
}) {
|
||||
thread.multi_agent_version = multi_agent_version;
|
||||
}
|
||||
thread.history = Some(StoredThreadHistory { thread_id, items });
|
||||
Ok(())
|
||||
}
|
||||
@@ -287,6 +300,11 @@ async fn stored_thread_from_sqlite_metadata(
|
||||
.ok()
|
||||
.map(|meta_line| meta_line.meta);
|
||||
let forked_from_id = session_meta.as_ref().and_then(|meta| meta.forked_from_id);
|
||||
let multi_agent_version = metadata.multi_agent_version.or_else(|| {
|
||||
session_meta
|
||||
.as_ref()
|
||||
.and_then(|meta| meta.multi_agent_version)
|
||||
});
|
||||
let preview = metadata
|
||||
.preview
|
||||
.clone()
|
||||
@@ -307,6 +325,7 @@ async fn stored_thread_from_sqlite_metadata(
|
||||
},
|
||||
model: metadata.model,
|
||||
reasoning_effort: metadata.reasoning_effort,
|
||||
multi_agent_version,
|
||||
created_at: metadata.created_at,
|
||||
updated_at: metadata.updated_at,
|
||||
archived_at: metadata.archived_at,
|
||||
@@ -370,6 +389,7 @@ fn stored_thread_from_meta_line(
|
||||
.unwrap_or_else(|| store.config.default_model_provider_id.clone()),
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
multi_agent_version: meta_line.meta.multi_agent_version,
|
||||
created_at,
|
||||
updated_at,
|
||||
archived_at: archived.then_some(updated_at),
|
||||
|
||||
@@ -213,6 +213,7 @@ async fn apply_metadata_update(
|
||||
patch.source.clone().unwrap_or(SessionSource::Unknown),
|
||||
);
|
||||
builder.model_provider = patch.model_provider.clone();
|
||||
builder.multi_agent_version = patch.multi_agent_version.flatten();
|
||||
builder.thread_source = patch.thread_source.flatten();
|
||||
builder.agent_nickname = patch.agent_nickname.clone().flatten();
|
||||
builder.agent_role = patch.agent_role.clone().flatten();
|
||||
@@ -246,6 +247,9 @@ async fn apply_metadata_update(
|
||||
if let Some(reasoning_effort) = patch.reasoning_effort {
|
||||
metadata.reasoning_effort = Some(reasoning_effort);
|
||||
}
|
||||
if let Some(multi_agent_version) = patch.multi_agent_version {
|
||||
metadata.multi_agent_version = multi_agent_version;
|
||||
}
|
||||
if let Some(created_at) = patch.created_at {
|
||||
metadata.created_at = created_at;
|
||||
}
|
||||
@@ -374,6 +378,7 @@ fn has_observed_metadata_facts(patch: &ThreadMetadataPatch) -> bool {
|
||||
|| patch.model_provider.is_some()
|
||||
|| patch.model.is_some()
|
||||
|| patch.reasoning_effort.is_some()
|
||||
|| patch.multi_agent_version.is_some()
|
||||
|| patch.created_at.is_some()
|
||||
|| patch.source.is_some()
|
||||
|| patch.thread_source.is_some()
|
||||
@@ -1615,6 +1620,7 @@ mod tests {
|
||||
ThreadPersistenceMetadata {
|
||||
cwd: Some(std::env::current_dir().expect("cwd")),
|
||||
model_provider: "test-provider".to_string(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: ThreadMemoryMode::Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ impl ThreadMetadataSync {
|
||||
};
|
||||
let update = ThreadMetadataPatch {
|
||||
model_provider: Some(params.metadata.model_provider.clone()),
|
||||
multi_agent_version: Some(params.metadata.multi_agent_version),
|
||||
created_at: Some(created_at),
|
||||
updated_at: Some(created_at),
|
||||
source: Some(params.source.clone()),
|
||||
@@ -178,7 +179,10 @@ impl ThreadMetadataSync {
|
||||
self.observe_items_with_update(
|
||||
items,
|
||||
ThreadMetadataPatch {
|
||||
updated_at: Some(Utc::now()),
|
||||
updated_at: items
|
||||
.iter()
|
||||
.any(|item| !matches!(item, RolloutItem::SessionMeta(_)))
|
||||
.then(Utc::now),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -205,6 +209,7 @@ impl ThreadMetadataSync {
|
||||
update.agent_nickname = Some(meta_line.meta.agent_nickname.clone());
|
||||
update.agent_role = Some(meta_line.meta.agent_role.clone());
|
||||
update.agent_path = Some(meta_line.meta.agent_path.clone());
|
||||
update.multi_agent_version = Some(meta_line.meta.multi_agent_version);
|
||||
if let Some(model_provider) = meta_line.meta.model_provider.clone()
|
||||
&& !model_provider.is_empty()
|
||||
{
|
||||
@@ -345,6 +350,7 @@ fn update_has_metadata_facts(update: &ThreadMetadataPatch) -> bool {
|
||||
|| update.model_provider.is_some()
|
||||
|| update.model.is_some()
|
||||
|| update.reasoning_effort.is_some()
|
||||
|| update.multi_agent_version.is_some()
|
||||
|| update.created_at.is_some()
|
||||
|| update.source.is_some()
|
||||
|| update.thread_source.is_some()
|
||||
@@ -371,6 +377,7 @@ fn git_info_patch_from_observation(git_info: GitInfo) -> GitInfoPatch {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use codex_protocol::openai_models::MultiAgentVersion;
|
||||
use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
@@ -494,6 +501,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_metadata_only_append_does_not_touch_updated_at() {
|
||||
let thread_id = ThreadId::new();
|
||||
let mut sync = ThreadMetadataSync::for_resume(&resume_params(thread_id, Vec::new()));
|
||||
let mut session_meta = session_meta(thread_id);
|
||||
session_meta.meta.multi_agent_version = Some(MultiAgentVersion::V2);
|
||||
|
||||
let update = sync
|
||||
.observe_appended_items(&[RolloutItem::SessionMeta(session_meta)])
|
||||
.expect("session metadata should update selector");
|
||||
|
||||
assert_eq!(
|
||||
(update.patch.multi_agent_version, update.patch.updated_at,),
|
||||
(Some(Some(MultiAgentVersion::V2)), None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_history_waits_for_append_before_flushing_metadata() {
|
||||
let thread_id = ThreadId::new();
|
||||
@@ -527,6 +551,7 @@ mod tests {
|
||||
metadata: ThreadPersistenceMetadata {
|
||||
cwd: None,
|
||||
model_provider: "test-provider".to_string(),
|
||||
multi_agent_version: None,
|
||||
memory_mode: ThreadMemoryMode::Enabled,
|
||||
},
|
||||
event_persistence_mode: ThreadEventPersistenceMode::Limited,
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::MultiAgentVersion;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GitInfo;
|
||||
@@ -61,6 +62,8 @@ pub struct ThreadPersistenceMetadata {
|
||||
pub cwd: Option<PathBuf>,
|
||||
/// Model provider associated with the thread.
|
||||
pub model_provider: String,
|
||||
/// Thread-scoped multi-agent runtime selector.
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
/// Memory mode associated with the live thread.
|
||||
pub memory_mode: MemoryMode,
|
||||
}
|
||||
@@ -372,6 +375,8 @@ pub struct StoredThread {
|
||||
pub model: Option<String>,
|
||||
/// Latest observed reasoning effort, if known.
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
/// Thread-scoped multi-agent runtime selector.
|
||||
pub multi_agent_version: Option<MultiAgentVersion>,
|
||||
/// Thread creation timestamp.
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// Thread last-update timestamp.
|
||||
@@ -479,6 +484,13 @@ pub struct ThreadMetadataPatch {
|
||||
pub model: Option<String>,
|
||||
/// Latest observed reasoning effort.
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
/// Thread-scoped multi-agent runtime selector.
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "optional_option"
|
||||
)]
|
||||
pub multi_agent_version: ClearableField<MultiAgentVersion>,
|
||||
/// Creation timestamp when known.
|
||||
pub created_at: Option<DateTime<Utc>>,
|
||||
/// Last update timestamp for this metadata observation.
|
||||
@@ -559,6 +571,9 @@ impl ThreadMetadataPatch {
|
||||
if next.reasoning_effort.is_some() {
|
||||
self.reasoning_effort = next.reasoning_effort;
|
||||
}
|
||||
if next.multi_agent_version.is_some() {
|
||||
self.multi_agent_version = next.multi_agent_version;
|
||||
}
|
||||
if next.created_at.is_some() {
|
||||
self.created_at = next.created_at;
|
||||
}
|
||||
@@ -616,6 +631,7 @@ impl ThreadMetadataPatch {
|
||||
&& self.model_provider.is_none()
|
||||
&& self.model.is_none()
|
||||
&& self.reasoning_effort.is_none()
|
||||
&& self.multi_agent_version.is_none()
|
||||
&& self.created_at.is_none()
|
||||
&& self.updated_at.is_none()
|
||||
&& self.source.is_none()
|
||||
|
||||
Reference in New Issue
Block a user