Separate interactive and non-interactive sessions (#4612)
Do not show exec session in VSCode/TUI selector.
This commit is contained in:
@@ -53,6 +53,7 @@ use codex_core::AuthManager;
|
||||
use codex_core::CodexConversation;
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::Cursor as RolloutCursor;
|
||||
use codex_core::INTERACTIVE_SESSION_SOURCES;
|
||||
use codex_core::NewConversation;
|
||||
use codex_core::RolloutRecorder;
|
||||
use codex_core::SessionMeta;
|
||||
@@ -708,6 +709,7 @@ impl CodexMessageProcessor {
|
||||
&self.config.codex_home,
|
||||
page_size,
|
||||
cursor_ref,
|
||||
INTERACTIVE_SESSION_SOURCES,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_core::ConversationManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::default_client::USER_AGENT_SUFFIX;
|
||||
use codex_core::default_client::get_codex_user_agent;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) struct MessageProcessor {
|
||||
@@ -35,7 +36,10 @@ impl MessageProcessor {
|
||||
) -> Self {
|
||||
let outgoing = Arc::new(outgoing);
|
||||
let auth_manager = AuthManager::shared(config.codex_home.clone(), false);
|
||||
let conversation_manager = Arc::new(ConversationManager::new(auth_manager.clone()));
|
||||
let conversation_manager = Arc::new(ConversationManager::new(
|
||||
auth_manager.clone(),
|
||||
SessionSource::VSCode,
|
||||
));
|
||||
let codex_message_processor = CodexMessageProcessor::new(
|
||||
auth_manager,
|
||||
conversation_manager,
|
||||
|
||||
@@ -22,6 +22,7 @@ use codex_protocol::protocol::ConversationPathResponseEvent;
|
||||
use codex_protocol::protocol::ExitedReviewModeEvent;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::TaskStartedEvent;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
@@ -172,6 +173,7 @@ impl Codex {
|
||||
config: Config,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
conversation_history: InitialHistory,
|
||||
session_source: SessionSource,
|
||||
) -> CodexResult<CodexSpawnOk> {
|
||||
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (tx_event, rx_event) = async_channel::unbounded();
|
||||
@@ -200,6 +202,7 @@ impl Codex {
|
||||
auth_manager.clone(),
|
||||
tx_event.clone(),
|
||||
conversation_history,
|
||||
session_source,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -334,6 +337,7 @@ impl Session {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
tx_event: Sender<Event>,
|
||||
initial_history: InitialHistory,
|
||||
session_source: SessionSource,
|
||||
) -> anyhow::Result<(Arc<Self>, TurnContext)> {
|
||||
let ConfigureSession {
|
||||
provider,
|
||||
@@ -357,7 +361,11 @@ impl Session {
|
||||
let conversation_id = ConversationId::default();
|
||||
(
|
||||
conversation_id,
|
||||
RolloutRecorderParams::new(conversation_id, user_instructions.clone()),
|
||||
RolloutRecorderParams::new(
|
||||
conversation_id,
|
||||
user_instructions.clone(),
|
||||
session_source,
|
||||
),
|
||||
)
|
||||
}
|
||||
InitialHistory::Resumed(resumed_history) => (
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_protocol::ConversationId;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@@ -35,20 +36,25 @@ pub struct NewConversation {
|
||||
pub struct ConversationManager {
|
||||
conversations: Arc<RwLock<HashMap<ConversationId, Arc<CodexConversation>>>>,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
session_source: SessionSource,
|
||||
}
|
||||
|
||||
impl ConversationManager {
|
||||
pub fn new(auth_manager: Arc<AuthManager>) -> Self {
|
||||
pub fn new(auth_manager: Arc<AuthManager>, session_source: SessionSource) -> Self {
|
||||
Self {
|
||||
conversations: Arc::new(RwLock::new(HashMap::new())),
|
||||
auth_manager,
|
||||
session_source,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct with a dummy AuthManager containing the provided CodexAuth.
|
||||
/// Used for integration tests: should not be used by ordinary business logic.
|
||||
pub fn with_auth(auth: CodexAuth) -> Self {
|
||||
Self::new(crate::AuthManager::from_auth_for_testing(auth))
|
||||
Self::new(
|
||||
crate::AuthManager::from_auth_for_testing(auth),
|
||||
SessionSource::Exec,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn new_conversation(&self, config: Config) -> CodexResult<NewConversation> {
|
||||
@@ -64,7 +70,13 @@ impl ConversationManager {
|
||||
let CodexSpawnOk {
|
||||
codex,
|
||||
conversation_id,
|
||||
} = Codex::spawn(config, auth_manager, InitialHistory::New).await?;
|
||||
} = Codex::spawn(
|
||||
config,
|
||||
auth_manager,
|
||||
InitialHistory::New,
|
||||
self.session_source,
|
||||
)
|
||||
.await?;
|
||||
self.finalize_spawn(codex, conversation_id).await
|
||||
}
|
||||
|
||||
@@ -121,7 +133,7 @@ impl ConversationManager {
|
||||
let CodexSpawnOk {
|
||||
codex,
|
||||
conversation_id,
|
||||
} = Codex::spawn(config, auth_manager, initial_history).await?;
|
||||
} = Codex::spawn(config, auth_manager, initial_history, self.session_source).await?;
|
||||
self.finalize_spawn(codex, conversation_id).await
|
||||
}
|
||||
|
||||
@@ -155,7 +167,7 @@ impl ConversationManager {
|
||||
let CodexSpawnOk {
|
||||
codex,
|
||||
conversation_id,
|
||||
} = Codex::spawn(config, auth_manager, history).await?;
|
||||
} = Codex::spawn(config, auth_manager, history, self.session_source).await?;
|
||||
|
||||
self.finalize_spawn(codex, conversation_id).await
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ pub mod terminal;
|
||||
mod tool_apply_patch;
|
||||
pub mod turn_diff_tracker;
|
||||
pub use rollout::ARCHIVED_SESSIONS_SUBDIR;
|
||||
pub use rollout::INTERACTIVE_SESSION_SOURCES;
|
||||
pub use rollout::RolloutRecorder;
|
||||
pub use rollout::SESSIONS_SUBDIR;
|
||||
pub use rollout::SessionMeta;
|
||||
|
||||
@@ -17,6 +17,7 @@ use super::SESSIONS_SUBDIR;
|
||||
use crate::protocol::EventMsg;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
|
||||
/// Returned page of conversation summaries.
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
@@ -52,6 +53,7 @@ struct HeadTailSummary {
|
||||
tail: Vec<serde_json::Value>,
|
||||
saw_session_meta: bool,
|
||||
saw_user_event: bool,
|
||||
source: Option<SessionSource>,
|
||||
created_at: Option<String>,
|
||||
updated_at: Option<String>,
|
||||
}
|
||||
@@ -106,6 +108,7 @@ pub(crate) async fn get_conversations(
|
||||
codex_home: &Path,
|
||||
page_size: usize,
|
||||
cursor: Option<&Cursor>,
|
||||
allowed_sources: &[SessionSource],
|
||||
) -> io::Result<ConversationsPage> {
|
||||
let mut root = codex_home.to_path_buf();
|
||||
root.push(SESSIONS_SUBDIR);
|
||||
@@ -121,7 +124,8 @@ pub(crate) async fn get_conversations(
|
||||
|
||||
let anchor = cursor.cloned();
|
||||
|
||||
let result = traverse_directories_for_paths(root.clone(), page_size, anchor).await?;
|
||||
let result =
|
||||
traverse_directories_for_paths(root.clone(), page_size, anchor, allowed_sources).await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -140,6 +144,7 @@ async fn traverse_directories_for_paths(
|
||||
root: PathBuf,
|
||||
page_size: usize,
|
||||
anchor: Option<Cursor>,
|
||||
allowed_sources: &[SessionSource],
|
||||
) -> io::Result<ConversationsPage> {
|
||||
let mut items: Vec<ConversationItem> = Vec::with_capacity(page_size);
|
||||
let mut scanned_files = 0usize;
|
||||
@@ -196,6 +201,13 @@ async fn traverse_directories_for_paths(
|
||||
let summary = read_head_and_tail(&path, HEAD_RECORD_LIMIT, TAIL_RECORD_LIMIT)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if !allowed_sources.is_empty()
|
||||
&& !summary
|
||||
.source
|
||||
.is_some_and(|source| allowed_sources.iter().any(|s| s == &source))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Apply filters: must have session meta and at least one user message event
|
||||
if summary.saw_session_meta && summary.saw_user_event {
|
||||
let HeadTailSummary {
|
||||
@@ -341,6 +353,7 @@ async fn read_head_and_tail(
|
||||
|
||||
match rollout_line.item {
|
||||
RolloutItem::SessionMeta(session_meta_line) => {
|
||||
summary.source = Some(session_meta_line.meta.source);
|
||||
summary.created_at = summary
|
||||
.created_at
|
||||
.clone()
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
//! Rollout module: persistence and discovery of session rollout files.
|
||||
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
|
||||
pub const SESSIONS_SUBDIR: &str = "sessions";
|
||||
pub const ARCHIVED_SESSIONS_SUBDIR: &str = "archived_sessions";
|
||||
pub const INTERACTIVE_SESSION_SOURCES: &[SessionSource] =
|
||||
&[SessionSource::Cli, SessionSource::VSCode];
|
||||
|
||||
pub mod list;
|
||||
pub(crate) mod policy;
|
||||
|
||||
@@ -32,6 +32,7 @@ use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
|
||||
/// Records all [`ResponseItem`]s for a session and flushes them to disk after
|
||||
/// every update.
|
||||
@@ -53,6 +54,7 @@ pub enum RolloutRecorderParams {
|
||||
Create {
|
||||
conversation_id: ConversationId,
|
||||
instructions: Option<String>,
|
||||
source: SessionSource,
|
||||
},
|
||||
Resume {
|
||||
path: PathBuf,
|
||||
@@ -71,10 +73,15 @@ enum RolloutCmd {
|
||||
}
|
||||
|
||||
impl RolloutRecorderParams {
|
||||
pub fn new(conversation_id: ConversationId, instructions: Option<String>) -> Self {
|
||||
pub fn new(
|
||||
conversation_id: ConversationId,
|
||||
instructions: Option<String>,
|
||||
source: SessionSource,
|
||||
) -> Self {
|
||||
Self::Create {
|
||||
conversation_id,
|
||||
instructions,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,8 +96,9 @@ impl RolloutRecorder {
|
||||
codex_home: &Path,
|
||||
page_size: usize,
|
||||
cursor: Option<&Cursor>,
|
||||
allowed_sources: &[SessionSource],
|
||||
) -> std::io::Result<ConversationsPage> {
|
||||
get_conversations(codex_home, page_size, cursor).await
|
||||
get_conversations(codex_home, page_size, cursor, allowed_sources).await
|
||||
}
|
||||
|
||||
/// Attempt to create a new [`RolloutRecorder`]. If the sessions directory
|
||||
@@ -101,6 +109,7 @@ impl RolloutRecorder {
|
||||
RolloutRecorderParams::Create {
|
||||
conversation_id,
|
||||
instructions,
|
||||
source,
|
||||
} => {
|
||||
let LogFileInfo {
|
||||
file,
|
||||
@@ -127,6 +136,7 @@ impl RolloutRecorder {
|
||||
originator: originator().value.clone(),
|
||||
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
instructions,
|
||||
source,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use time::format_description::FormatItem;
|
||||
use time::macros::format_description;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::rollout::INTERACTIVE_SESSION_SOURCES;
|
||||
use crate::rollout::list::ConversationItem;
|
||||
use crate::rollout::list::ConversationsPage;
|
||||
use crate::rollout::list::Cursor;
|
||||
@@ -28,13 +29,17 @@ use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SessionMeta;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
|
||||
const NO_SOURCE_FILTER: &[SessionSource] = &[];
|
||||
|
||||
fn write_session_file(
|
||||
root: &Path,
|
||||
ts_str: &str,
|
||||
uuid: Uuid,
|
||||
num_records: usize,
|
||||
source: Option<SessionSource>,
|
||||
) -> std::io::Result<(OffsetDateTime, Uuid)> {
|
||||
let format: &[FormatItem] =
|
||||
format_description!("[year]-[month]-[day]T[hour]-[minute]-[second]");
|
||||
@@ -52,17 +57,23 @@ fn write_session_file(
|
||||
let file_path = dir.join(filename);
|
||||
let mut file = File::create(file_path)?;
|
||||
|
||||
let mut payload = serde_json::json!({
|
||||
"id": uuid,
|
||||
"timestamp": ts_str,
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version",
|
||||
});
|
||||
|
||||
if let Some(source) = source {
|
||||
payload["source"] = serde_json::to_value(source).unwrap();
|
||||
}
|
||||
|
||||
let meta = serde_json::json!({
|
||||
"timestamp": ts_str,
|
||||
"type": "session_meta",
|
||||
"payload": {
|
||||
"id": uuid,
|
||||
"timestamp": ts_str,
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
}
|
||||
"payload": payload,
|
||||
});
|
||||
writeln!(file, "{meta}")?;
|
||||
|
||||
@@ -99,11 +110,34 @@ async fn test_list_conversations_latest_first() {
|
||||
let u3 = Uuid::from_u128(3);
|
||||
|
||||
// Create three sessions across three days
|
||||
write_session_file(home, "2025-01-01T12-00-00", u1, 3).unwrap();
|
||||
write_session_file(home, "2025-01-02T12-00-00", u2, 3).unwrap();
|
||||
write_session_file(home, "2025-01-03T12-00-00", u3, 3).unwrap();
|
||||
write_session_file(
|
||||
home,
|
||||
"2025-01-01T12-00-00",
|
||||
u1,
|
||||
3,
|
||||
Some(SessionSource::VSCode),
|
||||
)
|
||||
.unwrap();
|
||||
write_session_file(
|
||||
home,
|
||||
"2025-01-02T12-00-00",
|
||||
u2,
|
||||
3,
|
||||
Some(SessionSource::VSCode),
|
||||
)
|
||||
.unwrap();
|
||||
write_session_file(
|
||||
home,
|
||||
"2025-01-03T12-00-00",
|
||||
u3,
|
||||
3,
|
||||
Some(SessionSource::VSCode),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let page = get_conversations(home, 10, None).await.unwrap();
|
||||
let page = get_conversations(home, 10, None, INTERACTIVE_SESSION_SOURCES)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Build expected objects
|
||||
let p1 = home
|
||||
@@ -131,7 +165,8 @@ async fn test_list_conversations_latest_first() {
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
"cli_version": "test_version",
|
||||
"source": "vscode",
|
||||
})];
|
||||
let head_2 = vec![serde_json::json!({
|
||||
"id": u2,
|
||||
@@ -139,7 +174,8 @@ async fn test_list_conversations_latest_first() {
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
"cli_version": "test_version",
|
||||
"source": "vscode",
|
||||
})];
|
||||
let head_1 = vec![serde_json::json!({
|
||||
"id": u1,
|
||||
@@ -147,7 +183,8 @@ async fn test_list_conversations_latest_first() {
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
"cli_version": "test_version",
|
||||
"source": "vscode",
|
||||
})];
|
||||
|
||||
let expected_cursor: Cursor =
|
||||
@@ -198,13 +235,50 @@ async fn test_pagination_cursor() {
|
||||
let u5 = Uuid::from_u128(55);
|
||||
|
||||
// Oldest to newest
|
||||
write_session_file(home, "2025-03-01T09-00-00", u1, 1).unwrap();
|
||||
write_session_file(home, "2025-03-02T09-00-00", u2, 1).unwrap();
|
||||
write_session_file(home, "2025-03-03T09-00-00", u3, 1).unwrap();
|
||||
write_session_file(home, "2025-03-04T09-00-00", u4, 1).unwrap();
|
||||
write_session_file(home, "2025-03-05T09-00-00", u5, 1).unwrap();
|
||||
write_session_file(
|
||||
home,
|
||||
"2025-03-01T09-00-00",
|
||||
u1,
|
||||
1,
|
||||
Some(SessionSource::VSCode),
|
||||
)
|
||||
.unwrap();
|
||||
write_session_file(
|
||||
home,
|
||||
"2025-03-02T09-00-00",
|
||||
u2,
|
||||
1,
|
||||
Some(SessionSource::VSCode),
|
||||
)
|
||||
.unwrap();
|
||||
write_session_file(
|
||||
home,
|
||||
"2025-03-03T09-00-00",
|
||||
u3,
|
||||
1,
|
||||
Some(SessionSource::VSCode),
|
||||
)
|
||||
.unwrap();
|
||||
write_session_file(
|
||||
home,
|
||||
"2025-03-04T09-00-00",
|
||||
u4,
|
||||
1,
|
||||
Some(SessionSource::VSCode),
|
||||
)
|
||||
.unwrap();
|
||||
write_session_file(
|
||||
home,
|
||||
"2025-03-05T09-00-00",
|
||||
u5,
|
||||
1,
|
||||
Some(SessionSource::VSCode),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let page1 = get_conversations(home, 2, None).await.unwrap();
|
||||
let page1 = get_conversations(home, 2, None, INTERACTIVE_SESSION_SOURCES)
|
||||
.await
|
||||
.unwrap();
|
||||
let p5 = home
|
||||
.join("sessions")
|
||||
.join("2025")
|
||||
@@ -223,7 +297,8 @@ async fn test_pagination_cursor() {
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
"cli_version": "test_version",
|
||||
"source": "vscode",
|
||||
})];
|
||||
let head_4 = vec![serde_json::json!({
|
||||
"id": u4,
|
||||
@@ -231,7 +306,8 @@ async fn test_pagination_cursor() {
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
"cli_version": "test_version",
|
||||
"source": "vscode",
|
||||
})];
|
||||
let expected_cursor1: Cursor =
|
||||
serde_json::from_str(&format!("\"2025-03-04T09-00-00|{u4}\"")).unwrap();
|
||||
@@ -258,9 +334,14 @@ async fn test_pagination_cursor() {
|
||||
};
|
||||
assert_eq!(page1, expected_page1);
|
||||
|
||||
let page2 = get_conversations(home, 2, page1.next_cursor.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
let page2 = get_conversations(
|
||||
home,
|
||||
2,
|
||||
page1.next_cursor.as_ref(),
|
||||
INTERACTIVE_SESSION_SOURCES,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let p3 = home
|
||||
.join("sessions")
|
||||
.join("2025")
|
||||
@@ -279,7 +360,8 @@ async fn test_pagination_cursor() {
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
"cli_version": "test_version",
|
||||
"source": "vscode",
|
||||
})];
|
||||
let head_2 = vec![serde_json::json!({
|
||||
"id": u2,
|
||||
@@ -287,7 +369,8 @@ async fn test_pagination_cursor() {
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
"cli_version": "test_version",
|
||||
"source": "vscode",
|
||||
})];
|
||||
let expected_cursor2: Cursor =
|
||||
serde_json::from_str(&format!("\"2025-03-02T09-00-00|{u2}\"")).unwrap();
|
||||
@@ -314,9 +397,14 @@ async fn test_pagination_cursor() {
|
||||
};
|
||||
assert_eq!(page2, expected_page2);
|
||||
|
||||
let page3 = get_conversations(home, 2, page2.next_cursor.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
let page3 = get_conversations(
|
||||
home,
|
||||
2,
|
||||
page2.next_cursor.as_ref(),
|
||||
INTERACTIVE_SESSION_SOURCES,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let p1 = home
|
||||
.join("sessions")
|
||||
.join("2025")
|
||||
@@ -329,7 +417,8 @@ async fn test_pagination_cursor() {
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
"cli_version": "test_version",
|
||||
"source": "vscode",
|
||||
})];
|
||||
let expected_cursor3: Cursor =
|
||||
serde_json::from_str(&format!("\"2025-03-01T09-00-00|{u1}\"")).unwrap();
|
||||
@@ -355,9 +444,11 @@ async fn test_get_conversation_contents() {
|
||||
|
||||
let uuid = Uuid::new_v4();
|
||||
let ts = "2025-04-01T10-30-00";
|
||||
write_session_file(home, ts, uuid, 2).unwrap();
|
||||
write_session_file(home, ts, uuid, 2, Some(SessionSource::VSCode)).unwrap();
|
||||
|
||||
let page = get_conversations(home, 1, None).await.unwrap();
|
||||
let page = get_conversations(home, 1, None, INTERACTIVE_SESSION_SOURCES)
|
||||
.await
|
||||
.unwrap();
|
||||
let path = &page.items[0].path;
|
||||
|
||||
let content = get_conversation(path).await.unwrap();
|
||||
@@ -375,7 +466,8 @@ async fn test_get_conversation_contents() {
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
"cli_version": "test_version",
|
||||
"source": "vscode",
|
||||
})];
|
||||
let expected_cursor: Cursor = serde_json::from_str(&format!("\"{ts}|{uuid}\"")).unwrap();
|
||||
let expected_page = ConversationsPage {
|
||||
@@ -393,7 +485,19 @@ async fn test_get_conversation_contents() {
|
||||
assert_eq!(page, expected_page);
|
||||
|
||||
// Entire file contents equality
|
||||
let meta = serde_json::json!({"timestamp": ts, "type": "session_meta", "payload": {"id": uuid, "timestamp": ts, "instructions": null, "cwd": ".", "originator": "test_originator", "cli_version": "test_version"}});
|
||||
let meta = serde_json::json!({
|
||||
"timestamp": ts,
|
||||
"type": "session_meta",
|
||||
"payload": {
|
||||
"id": uuid,
|
||||
"timestamp": ts,
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version",
|
||||
"source": "vscode",
|
||||
}
|
||||
});
|
||||
let user_event = serde_json::json!({
|
||||
"timestamp": ts,
|
||||
"type": "event_msg",
|
||||
@@ -428,6 +532,7 @@ async fn test_tail_includes_last_response_items() -> Result<()> {
|
||||
cwd: ".".into(),
|
||||
originator: "test_originator".into(),
|
||||
cli_version: "test_version".into(),
|
||||
source: SessionSource::VSCode,
|
||||
},
|
||||
git: None,
|
||||
}),
|
||||
@@ -460,7 +565,7 @@ async fn test_tail_includes_last_response_items() -> Result<()> {
|
||||
}
|
||||
drop(file);
|
||||
|
||||
let page = get_conversations(home, 1, None).await?;
|
||||
let page = get_conversations(home, 1, None, INTERACTIVE_SESSION_SOURCES).await?;
|
||||
let item = page.items.first().expect("conversation item");
|
||||
let tail_len = item.tail.len();
|
||||
assert_eq!(tail_len, 10usize.min(total_messages));
|
||||
@@ -511,6 +616,7 @@ async fn test_tail_handles_short_sessions() -> Result<()> {
|
||||
cwd: ".".into(),
|
||||
originator: "test_originator".into(),
|
||||
cli_version: "test_version".into(),
|
||||
source: SessionSource::VSCode,
|
||||
},
|
||||
git: None,
|
||||
}),
|
||||
@@ -542,7 +648,7 @@ async fn test_tail_handles_short_sessions() -> Result<()> {
|
||||
}
|
||||
drop(file);
|
||||
|
||||
let page = get_conversations(home, 1, None).await?;
|
||||
let page = get_conversations(home, 1, None, INTERACTIVE_SESSION_SOURCES).await?;
|
||||
let tail = &page.items.first().expect("conversation item").tail;
|
||||
|
||||
assert_eq!(tail.len(), 3);
|
||||
@@ -595,6 +701,7 @@ async fn test_tail_skips_trailing_non_responses() -> Result<()> {
|
||||
cwd: ".".into(),
|
||||
originator: "test_originator".into(),
|
||||
cli_version: "test_version".into(),
|
||||
source: SessionSource::VSCode,
|
||||
},
|
||||
git: None,
|
||||
}),
|
||||
@@ -640,7 +747,7 @@ async fn test_tail_skips_trailing_non_responses() -> Result<()> {
|
||||
writeln!(file, "{}", serde_json::to_string(&shutdown_event)?)?;
|
||||
drop(file);
|
||||
|
||||
let page = get_conversations(home, 1, None).await?;
|
||||
let page = get_conversations(home, 1, None, INTERACTIVE_SESSION_SOURCES).await?;
|
||||
let tail = &page.items.first().expect("conversation item").tail;
|
||||
|
||||
let expected: Vec<serde_json::Value> = (0..4)
|
||||
@@ -678,11 +785,13 @@ async fn test_stable_ordering_same_second_pagination() {
|
||||
let u2 = Uuid::from_u128(2);
|
||||
let u3 = Uuid::from_u128(3);
|
||||
|
||||
write_session_file(home, ts, u1, 0).unwrap();
|
||||
write_session_file(home, ts, u2, 0).unwrap();
|
||||
write_session_file(home, ts, u3, 0).unwrap();
|
||||
write_session_file(home, ts, u1, 0, Some(SessionSource::VSCode)).unwrap();
|
||||
write_session_file(home, ts, u2, 0, Some(SessionSource::VSCode)).unwrap();
|
||||
write_session_file(home, ts, u3, 0, Some(SessionSource::VSCode)).unwrap();
|
||||
|
||||
let page1 = get_conversations(home, 2, None).await.unwrap();
|
||||
let page1 = get_conversations(home, 2, None, INTERACTIVE_SESSION_SOURCES)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let p3 = home
|
||||
.join("sessions")
|
||||
@@ -703,7 +812,8 @@ async fn test_stable_ordering_same_second_pagination() {
|
||||
"instructions": null,
|
||||
"cwd": ".",
|
||||
"originator": "test_originator",
|
||||
"cli_version": "test_version"
|
||||
"cli_version": "test_version",
|
||||
"source": "vscode",
|
||||
})]
|
||||
};
|
||||
let expected_cursor1: Cursor = serde_json::from_str(&format!("\"{ts}|{u2}\"")).unwrap();
|
||||
@@ -730,9 +840,14 @@ async fn test_stable_ordering_same_second_pagination() {
|
||||
};
|
||||
assert_eq!(page1, expected_page1);
|
||||
|
||||
let page2 = get_conversations(home, 2, page1.next_cursor.as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
let page2 = get_conversations(
|
||||
home,
|
||||
2,
|
||||
page1.next_cursor.as_ref(),
|
||||
INTERACTIVE_SESSION_SOURCES,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let p1 = home
|
||||
.join("sessions")
|
||||
.join("2025")
|
||||
@@ -754,3 +869,59 @@ async fn test_stable_ordering_same_second_pagination() {
|
||||
};
|
||||
assert_eq!(page2, expected_page2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_source_filter_excludes_non_matching_sessions() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let home = temp.path();
|
||||
|
||||
let interactive_id = Uuid::from_u128(42);
|
||||
let non_interactive_id = Uuid::from_u128(77);
|
||||
|
||||
write_session_file(
|
||||
home,
|
||||
"2025-08-02T10-00-00",
|
||||
interactive_id,
|
||||
2,
|
||||
Some(SessionSource::Cli),
|
||||
)
|
||||
.unwrap();
|
||||
write_session_file(
|
||||
home,
|
||||
"2025-08-01T10-00-00",
|
||||
non_interactive_id,
|
||||
2,
|
||||
Some(SessionSource::Exec),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let interactive_only = get_conversations(home, 10, None, INTERACTIVE_SESSION_SOURCES)
|
||||
.await
|
||||
.unwrap();
|
||||
let paths: Vec<_> = interactive_only
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| item.path.as_path())
|
||||
.collect();
|
||||
|
||||
assert_eq!(paths.len(), 1);
|
||||
assert!(paths.iter().all(|path| {
|
||||
path.ends_with("rollout-2025-08-02T10-00-00-00000000-0000-0000-0000-00000000002a.jsonl")
|
||||
}));
|
||||
|
||||
let all_sessions = get_conversations(home, 10, None, NO_SOURCE_FILTER)
|
||||
.await
|
||||
.unwrap();
|
||||
let all_paths: Vec<_> = all_sessions
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| item.path)
|
||||
.collect();
|
||||
assert_eq!(all_paths.len(), 2);
|
||||
assert!(all_paths.iter().any(|path| {
|
||||
path.ends_with("rollout-2025-08-02T10-00-00-00000000-0000-0000-0000-00000000002a.jsonl")
|
||||
}));
|
||||
assert!(all_paths.iter().any(|path| {
|
||||
path.ends_with("rollout-2025-08-01T10-00-00-00000000-0000-0000-0000-00000000004d.jsonl")
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ async fn chat_mode_stream_cli() {
|
||||
server.verify().await;
|
||||
|
||||
// Verify a new session rollout was created and is discoverable via list_conversations
|
||||
let page = RolloutRecorder::list_conversations(home.path(), 10, None)
|
||||
let page = RolloutRecorder::list_conversations(home.path(), 10, None, &[])
|
||||
.await
|
||||
.expect("list conversations");
|
||||
assert!(
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_core::built_in_model_providers;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::InputItem;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_otel::otel_event_manager::OtelEventManager;
|
||||
use codex_protocol::ConversationId;
|
||||
use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
@@ -538,7 +539,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
|
||||
Ok(None) => panic!("No CodexAuth found in codex_home"),
|
||||
Err(e) => panic!("Failed to load CodexAuth: {e}"),
|
||||
};
|
||||
let conversation_manager = ConversationManager::new(auth_manager);
|
||||
let conversation_manager = ConversationManager::new(auth_manager, SessionSource::Exec);
|
||||
let NewConversation {
|
||||
conversation: codex,
|
||||
..
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_core::protocol::Event;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::InputItem;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_core::protocol::TaskCompleteEvent;
|
||||
use codex_ollama::DEFAULT_OSS_MODEL;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
@@ -237,7 +238,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
||||
}
|
||||
|
||||
let auth_manager = AuthManager::shared(config.codex_home.clone(), true);
|
||||
let conversation_manager = ConversationManager::new(auth_manager.clone());
|
||||
let conversation_manager = ConversationManager::new(auth_manager.clone(), SessionSource::Exec);
|
||||
|
||||
// Handle resume subcommand by resolving a rollout path and using explicit resume API.
|
||||
let NewConversation {
|
||||
@@ -375,7 +376,9 @@ async fn resolve_resume_path(
|
||||
args: &crate::cli::ResumeArgs,
|
||||
) -> anyhow::Result<Option<PathBuf>> {
|
||||
if args.last {
|
||||
match codex_core::RolloutRecorder::list_conversations(&config.codex_home, 1, None).await {
|
||||
match codex_core::RolloutRecorder::list_conversations(&config.codex_home, 1, None, &[])
|
||||
.await
|
||||
{
|
||||
Ok(page) => Ok(page.items.first().map(|it| it.path.clone())),
|
||||
Err(e) => {
|
||||
error!("Error listing conversations: {e}");
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::codex_tool_config::create_tool_for_codex_tool_call_reply_param;
|
||||
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use codex_protocol::ConversationId;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::ConversationManager;
|
||||
@@ -53,7 +54,8 @@ impl MessageProcessor {
|
||||
) -> Self {
|
||||
let outgoing = Arc::new(outgoing);
|
||||
let auth_manager = AuthManager::shared(config.codex_home.clone(), false);
|
||||
let conversation_manager = Arc::new(ConversationManager::new(auth_manager));
|
||||
let conversation_manager =
|
||||
Arc::new(ConversationManager::new(auth_manager, SessionSource::Mcp));
|
||||
Self {
|
||||
outgoing,
|
||||
initialized: false,
|
||||
|
||||
@@ -919,7 +919,20 @@ impl InitialHistory {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default, Debug, TS)]
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, TS, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(rename_all = "lowercase")]
|
||||
pub enum SessionSource {
|
||||
Cli,
|
||||
#[default]
|
||||
VSCode,
|
||||
Exec,
|
||||
Mcp,
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, TS)]
|
||||
pub struct SessionMeta {
|
||||
pub id: ConversationId,
|
||||
pub timestamp: String,
|
||||
@@ -927,6 +940,22 @@ pub struct SessionMeta {
|
||||
pub originator: String,
|
||||
pub cli_version: String,
|
||||
pub instructions: Option<String>,
|
||||
#[serde(default)]
|
||||
pub source: SessionSource,
|
||||
}
|
||||
|
||||
impl Default for SessionMeta {
|
||||
fn default() -> Self {
|
||||
SessionMeta {
|
||||
id: ConversationId::default(),
|
||||
timestamp: String::new(),
|
||||
cwd: PathBuf::new(),
|
||||
originator: String::new(),
|
||||
cli_version: String::new(),
|
||||
instructions: None,
|
||||
source: SessionSource::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, TS)]
|
||||
|
||||
@@ -18,6 +18,7 @@ use codex_core::ConversationManager;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::persist_model_selection;
|
||||
use codex_core::model_family::find_family_for_model;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::ConversationId;
|
||||
@@ -86,7 +87,10 @@ impl App {
|
||||
let (app_event_tx, mut app_event_rx) = unbounded_channel();
|
||||
let app_event_tx = AppEventSender::new(app_event_tx);
|
||||
|
||||
let conversation_manager = Arc::new(ConversationManager::new(auth_manager.clone()));
|
||||
let conversation_manager = Arc::new(ConversationManager::new(
|
||||
auth_manager.clone(),
|
||||
SessionSource::Cli,
|
||||
));
|
||||
|
||||
let enhanced_keys_supported = tui.enhanced_keys_supported();
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_app_server_protocol::AuthMode;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::INTERACTIVE_SESSION_SOURCES;
|
||||
use codex_core::RolloutRecorder;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
@@ -393,7 +394,14 @@ async fn run_ratatui_app(
|
||||
}
|
||||
}
|
||||
} else if cli.resume_last {
|
||||
match RolloutRecorder::list_conversations(&config.codex_home, 1, None).await {
|
||||
match RolloutRecorder::list_conversations(
|
||||
&config.codex_home,
|
||||
1,
|
||||
None,
|
||||
INTERACTIVE_SESSION_SOURCES,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(page) => page
|
||||
.items
|
||||
.first()
|
||||
|
||||
@@ -8,6 +8,7 @@ use chrono::Utc;
|
||||
use codex_core::ConversationItem;
|
||||
use codex_core::ConversationsPage;
|
||||
use codex_core::Cursor;
|
||||
use codex_core::INTERACTIVE_SESSION_SOURCES;
|
||||
use codex_core::RolloutRecorder;
|
||||
use color_eyre::eyre::Result;
|
||||
use crossterm::event::KeyCode;
|
||||
@@ -77,6 +78,7 @@ pub async fn run_resume_picker(tui: &mut Tui, codex_home: &Path) -> Result<Resum
|
||||
&request.codex_home,
|
||||
PAGE_SIZE,
|
||||
request.cursor.as_ref(),
|
||||
INTERACTIVE_SESSION_SOURCES,
|
||||
)
|
||||
.await;
|
||||
let _ = tx.send(BackgroundEvent::PageLoaded {
|
||||
@@ -324,7 +326,13 @@ impl PickerState {
|
||||
}
|
||||
|
||||
async fn load_initial_page(&mut self) -> Result<()> {
|
||||
let page = RolloutRecorder::list_conversations(&self.codex_home, PAGE_SIZE, None).await?;
|
||||
let page = RolloutRecorder::list_conversations(
|
||||
&self.codex_home,
|
||||
PAGE_SIZE,
|
||||
None,
|
||||
INTERACTIVE_SESSION_SOURCES,
|
||||
)
|
||||
.await?;
|
||||
self.reset_pagination();
|
||||
self.all_rows.clear();
|
||||
self.filtered_rows.clear();
|
||||
|
||||
Reference in New Issue
Block a user