-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: external session loading (claude and codex) #5870
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements external session loading from Claude Code and Codex editors, controlled by a new GOOSE_ENABLE_EXTERNAL_SESSIONS configuration flag (off by default). When enabled, Goose can discover and display sessions from these external tools in the session list. The actual conversation history is only loaded when a user explicitly resumes a session.
Key Changes:
- Added UI toggle in settings to enable/disable external session access
- Integrated external session loading into session manager's fallback logic for
get_session()andlist_sessions() - Implemented parsers for Claude Code (
.claude/projects/) and Codex (.codex/sessions/) session file formats
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
ui/desktop/src/components/settings/sessions/SessionSharingSection.tsx |
Adds External Sessions card with toggle switch to enable/disable feature |
crates/goose/src/session/session_manager.rs |
Integrates external session fallback in get_session() and extends session list with external sessions |
crates/goose/src/session/mod.rs |
Adds module declarations for claude_code, codex, and external_sessions |
crates/goose/src/session/external_sessions.rs |
Implements feature flag check and orchestrates Claude Code and Codex session loading |
crates/goose/src/session/codex.rs |
Parses Codex JSONL session files from nested date-based directory structure |
crates/goose/src/session/claude_code.rs |
Parses Claude Code JSONL session files from project-based directory structure |
crates/goose/src/config/base.rs |
Adds GOOSE_ENABLE_EXTERNAL_SESSIONS boolean configuration value |
| fn get_claude_code_session(id: &str, include_messages: bool) -> Option<Session> { | ||
| let sessions = crate::session::claude_code::list_claude_code_sessions().ok()?; | ||
|
|
||
| for (session_id, working_dir, updated_at) in sessions { | ||
| if session_id == id { | ||
| let conversation = if include_messages { | ||
| crate::session::claude_code::load_claude_code_session(id).ok() | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| let message_count = conversation | ||
| .as_ref() | ||
| .map(|c| c.messages().len()) | ||
| .unwrap_or(0); | ||
|
|
||
| return Some(Session { | ||
| id: session_id.clone(), | ||
| working_dir, | ||
| name: format!( | ||
| "Claude Code Session {}", | ||
| session_id.chars().take(8).collect::<String>() | ||
| ), | ||
| user_set_name: false, | ||
| session_type: SessionType::User, | ||
| created_at: updated_at, | ||
| updated_at, | ||
| extension_data: ExtensionData::default(), | ||
| total_tokens: None, | ||
| input_tokens: None, | ||
| output_tokens: None, | ||
| accumulated_total_tokens: None, | ||
| accumulated_input_tokens: None, | ||
| accumulated_output_tokens: None, | ||
| schedule_id: None, | ||
| recipe: None, | ||
| user_recipe_values: None, | ||
| conversation, | ||
| message_count, | ||
| provider_name: None, | ||
| model_config: None, | ||
| }); | ||
| } | ||
| } | ||
| None | ||
| } | ||
|
|
||
| fn get_codex_session(id: &str, include_messages: bool) -> Option<Session> { | ||
| let sessions = crate::session::codex::list_codex_sessions().ok()?; | ||
|
|
||
| for (session_id, working_dir, updated_at) in sessions { | ||
| if session_id == id { | ||
| let conversation = if include_messages { | ||
| crate::session::codex::load_codex_session(id).ok() | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| let message_count = conversation | ||
| .as_ref() | ||
| .map(|c| c.messages().len()) | ||
| .unwrap_or(0); | ||
|
|
||
| return Some(Session { | ||
| id: session_id.clone(), | ||
| working_dir, | ||
| name: format!( | ||
| "Codex Session {}", | ||
| session_id.chars().take(8).collect::<String>() | ||
| ), | ||
| user_set_name: false, | ||
| session_type: SessionType::User, | ||
| created_at: updated_at, | ||
| updated_at, | ||
| extension_data: ExtensionData::default(), | ||
| total_tokens: None, | ||
| input_tokens: None, | ||
| output_tokens: None, | ||
| accumulated_total_tokens: None, | ||
| accumulated_input_tokens: None, | ||
| accumulated_output_tokens: None, | ||
| schedule_id: None, | ||
| recipe: None, | ||
| user_recipe_values: None, | ||
| conversation, | ||
| message_count, | ||
| provider_name: None, | ||
| model_config: None, | ||
| }); | ||
| } | ||
| } | ||
| None | ||
| } |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both get_claude_code_session and get_codex_session contain nearly identical logic with only the function calls and session name prefix differing. Extract this to a shared helper function to reduce duplication and improve maintainability.
| sessions.push(Session { | ||
| id: session_id.clone(), | ||
| working_dir, | ||
| name: format!( | ||
| "Claude Code Session {}", | ||
| session_id.chars().take(8).collect::<String>() | ||
| ), | ||
| user_set_name: false, | ||
| session_type: SessionType::User, | ||
| created_at: updated_at, | ||
| updated_at, | ||
| extension_data: ExtensionData::default(), | ||
| total_tokens: None, | ||
| input_tokens: None, | ||
| output_tokens: None, | ||
| accumulated_total_tokens: None, | ||
| accumulated_input_tokens: None, | ||
| accumulated_output_tokens: None, | ||
| schedule_id: None, | ||
| recipe: None, | ||
| user_recipe_values: None, | ||
| conversation: None, | ||
| message_count: 0, | ||
| provider_name: None, | ||
| model_config: None, | ||
| }); | ||
| } | ||
| } | ||
| Err(e) => { | ||
| tracing::debug!("Failed to list Claude Code sessions: {}", e); | ||
| } | ||
| } | ||
|
|
||
| match crate::session::codex::list_codex_sessions() { | ||
| Ok(codex_sessions) => { | ||
| tracing::debug!("Found {} Codex sessions", codex_sessions.len()); | ||
| for (session_id, working_dir, updated_at) in codex_sessions { | ||
| sessions.push(Session { | ||
| id: session_id.clone(), | ||
| working_dir, | ||
| name: format!( | ||
| "Codex Session {}", | ||
| session_id.chars().take(8).collect::<String>() | ||
| ), | ||
| user_set_name: false, | ||
| session_type: SessionType::User, | ||
| created_at: updated_at, | ||
| updated_at, | ||
| extension_data: ExtensionData::default(), | ||
| total_tokens: None, | ||
| input_tokens: None, | ||
| output_tokens: None, | ||
| accumulated_total_tokens: None, | ||
| accumulated_input_tokens: None, | ||
| accumulated_output_tokens: None, | ||
| schedule_id: None, | ||
| recipe: None, | ||
| user_recipe_values: None, | ||
| conversation: None, | ||
| message_count: 0, | ||
| provider_name: None, | ||
| model_config: None, | ||
| }); |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In get_external_sessions_for_list(), the Session construction logic is duplicated for both Claude Code and Codex sessions (lines 136-161 and 173-198). This is identical code except for the session name prefix. Extract this into a helper function that takes the session metadata and name prefix as parameters.
| for entry in std::fs::read_dir(projects_dir)? { | ||
| let entry = entry?; | ||
| let project_path = entry.path(); | ||
|
|
||
| if !project_path.is_dir() { | ||
| continue; | ||
| } | ||
|
|
||
| for file_entry in std::fs::read_dir(&project_path)? { | ||
| let file_entry = file_entry?; | ||
| let file_path = file_entry.path(); | ||
|
|
||
| if !file_path.is_file() { | ||
| continue; | ||
| } | ||
|
|
||
| let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or(""); | ||
|
|
||
| if file_name.starts_with("agent-") || !file_name.ends_with(".jsonl") { | ||
| continue; | ||
| } | ||
|
|
||
| if let Ok((session_id, working_dir, updated_at)) = parse_session_metadata(&file_path) { | ||
| tracing::debug!( | ||
| "Found Claude session: {} updated at {}", | ||
| session_id, | ||
| updated_at | ||
| ); | ||
| sessions.push((session_id, working_dir, updated_at)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| tracing::debug!("Total Claude sessions found: {}", sessions.len()); | ||
|
|
||
| sessions.sort_by(|a, b| b.2.cmp(&a.2)); | ||
| sessions.truncate(10); | ||
|
|
||
| Ok(sessions) | ||
| } |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nested directory traversal (2 levels deep) in list_claude_code_sessions() will be slow and I/O intensive. This code is called on every list_sessions() request. Consider caching the results or making this an opt-in background operation.
| pub fn load_claude_code_session(session_id: &str) -> Result<Conversation> { | ||
| let home = get_home_dir().ok_or_else(|| anyhow::anyhow!("No home dir"))?; | ||
| let projects_dir = home.join(".claude").join("projects"); | ||
|
|
||
| for entry in std::fs::read_dir(projects_dir)? { | ||
| let entry = entry?; | ||
| let project_path = entry.path(); | ||
|
|
||
| if !project_path.is_dir() { | ||
| continue; | ||
| } | ||
|
|
||
| for file_entry in std::fs::read_dir(&project_path)? { | ||
| let file_entry = file_entry?; | ||
| let file_path = file_entry.path(); | ||
|
|
||
| if !file_path.is_file() { | ||
| continue; | ||
| } | ||
|
|
||
| let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or(""); | ||
|
|
||
| if file_name.contains(session_id) && file_name.ends_with(".jsonl") { | ||
| return parse_conversation(&file_path); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Err(anyhow::anyhow!("Session not found")) | ||
| } |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The load_claude_code_session() function performs a full filesystem walk through potentially many directories and files every time it's called. Consider optimizing by either: (1) maintaining an index/cache of session file locations, or (2) deriving the file path from session metadata if possible.
| use crate::session::external_sessions; | ||
| sessions.extend(external_sessions::get_external_sessions_for_list()); | ||
| sessions.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_external_sessions_for_list() performs blocking file I/O but is called from an async context in list_sessions(). This will block the tokio runtime thread. Wrap the synchronous I/O calls in tokio::task::spawn_blocking() to prevent blocking the async runtime.
| let sessions = crate::session::claude_code::list_claude_code_sessions().ok()?; | ||
|
|
||
| for (session_id, working_dir, updated_at) in sessions { | ||
| if session_id == id { | ||
| let conversation = if include_messages { | ||
| crate::session::claude_code::load_claude_code_session(id).ok() | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| let message_count = conversation | ||
| .as_ref() | ||
| .map(|c| c.messages().len()) | ||
| .unwrap_or(0); | ||
|
|
||
| return Some(Session { | ||
| id: session_id.clone(), | ||
| working_dir, | ||
| name: format!( | ||
| "Claude Code Session {}", | ||
| session_id.chars().take(8).collect::<String>() | ||
| ), | ||
| user_set_name: false, | ||
| session_type: SessionType::User, | ||
| created_at: updated_at, | ||
| updated_at, | ||
| extension_data: ExtensionData::default(), | ||
| total_tokens: None, | ||
| input_tokens: None, | ||
| output_tokens: None, | ||
| accumulated_total_tokens: None, | ||
| accumulated_input_tokens: None, | ||
| accumulated_output_tokens: None, | ||
| schedule_id: None, | ||
| recipe: None, | ||
| user_recipe_values: None, | ||
| conversation, | ||
| message_count, | ||
| provider_name: None, | ||
| model_config: None, | ||
| }); | ||
| } | ||
| } | ||
| None |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_claude_code_session lists all sessions and iterates through them to find a match. This is inefficient - when include_messages is true, it calls list_claude_code_sessions() (which walks the entire filesystem), then calls load_claude_code_session() (which walks the filesystem again). Consider having load_claude_code_session directly locate and load the session file by ID.
| let sessions = crate::session::claude_code::list_claude_code_sessions().ok()?; | |
| for (session_id, working_dir, updated_at) in sessions { | |
| if session_id == id { | |
| let conversation = if include_messages { | |
| crate::session::claude_code::load_claude_code_session(id).ok() | |
| } else { | |
| None | |
| }; | |
| let message_count = conversation | |
| .as_ref() | |
| .map(|c| c.messages().len()) | |
| .unwrap_or(0); | |
| return Some(Session { | |
| id: session_id.clone(), | |
| working_dir, | |
| name: format!( | |
| "Claude Code Session {}", | |
| session_id.chars().take(8).collect::<String>() | |
| ), | |
| user_set_name: false, | |
| session_type: SessionType::User, | |
| created_at: updated_at, | |
| updated_at, | |
| extension_data: ExtensionData::default(), | |
| total_tokens: None, | |
| input_tokens: None, | |
| output_tokens: None, | |
| accumulated_total_tokens: None, | |
| accumulated_input_tokens: None, | |
| accumulated_output_tokens: None, | |
| schedule_id: None, | |
| recipe: None, | |
| user_recipe_values: None, | |
| conversation, | |
| message_count, | |
| provider_name: None, | |
| model_config: None, | |
| }); | |
| } | |
| } | |
| None | |
| // Try to load the session directly by ID | |
| let loaded = crate::session::claude_code::load_claude_code_session(id).ok()?; | |
| // Assume load_claude_code_session returns a struct with working_dir, updated_at, created_at, etc. | |
| let message_count = loaded.messages().len(); | |
| Some(Session { | |
| id: id.to_string(), | |
| working_dir: loaded.working_dir().clone(), | |
| name: format!( | |
| "Claude Code Session {}", | |
| id.chars().take(8).collect::<String>() | |
| ), | |
| user_set_name: false, | |
| session_type: SessionType::User, | |
| created_at: loaded.created_at(), | |
| updated_at: loaded.updated_at(), | |
| extension_data: ExtensionData::default(), | |
| total_tokens: None, | |
| input_tokens: None, | |
| output_tokens: None, | |
| accumulated_total_tokens: None, | |
| accumulated_input_tokens: None, | |
| accumulated_output_tokens: None, | |
| schedule_id: None, | |
| recipe: None, | |
| user_recipe_values: None, | |
| conversation: if include_messages { Some(loaded) } else { None }, | |
| message_count, | |
| provider_name: None, | |
| model_config: None, | |
| }) |
| let sessions = crate::session::codex::list_codex_sessions().ok()?; | ||
|
|
||
| for (session_id, working_dir, updated_at) in sessions { | ||
| if session_id == id { | ||
| let conversation = if include_messages { | ||
| crate::session::codex::load_codex_session(id).ok() | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| let message_count = conversation | ||
| .as_ref() | ||
| .map(|c| c.messages().len()) | ||
| .unwrap_or(0); | ||
|
|
||
| return Some(Session { | ||
| id: session_id.clone(), | ||
| working_dir, | ||
| name: format!( | ||
| "Codex Session {}", | ||
| session_id.chars().take(8).collect::<String>() | ||
| ), | ||
| user_set_name: false, | ||
| session_type: SessionType::User, | ||
| created_at: updated_at, | ||
| updated_at, | ||
| extension_data: ExtensionData::default(), | ||
| total_tokens: None, | ||
| input_tokens: None, | ||
| output_tokens: None, | ||
| accumulated_total_tokens: None, | ||
| accumulated_input_tokens: None, | ||
| accumulated_output_tokens: None, | ||
| schedule_id: None, | ||
| recipe: None, | ||
| user_recipe_values: None, | ||
| conversation, | ||
| message_count, | ||
| provider_name: None, | ||
| model_config: None, | ||
| }); | ||
| } | ||
| } | ||
| None |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_codex_session lists all sessions and iterates through them to find a match. This is inefficient - when include_messages is true, it calls list_codex_sessions() (which walks the entire filesystem), then calls load_codex_session() (which walks the filesystem again). Consider having load_codex_session directly locate and load the session file by ID.
| let sessions = crate::session::codex::list_codex_sessions().ok()?; | |
| for (session_id, working_dir, updated_at) in sessions { | |
| if session_id == id { | |
| let conversation = if include_messages { | |
| crate::session::codex::load_codex_session(id).ok() | |
| } else { | |
| None | |
| }; | |
| let message_count = conversation | |
| .as_ref() | |
| .map(|c| c.messages().len()) | |
| .unwrap_or(0); | |
| return Some(Session { | |
| id: session_id.clone(), | |
| working_dir, | |
| name: format!( | |
| "Codex Session {}", | |
| session_id.chars().take(8).collect::<String>() | |
| ), | |
| user_set_name: false, | |
| session_type: SessionType::User, | |
| created_at: updated_at, | |
| updated_at, | |
| extension_data: ExtensionData::default(), | |
| total_tokens: None, | |
| input_tokens: None, | |
| output_tokens: None, | |
| accumulated_total_tokens: None, | |
| accumulated_input_tokens: None, | |
| accumulated_output_tokens: None, | |
| schedule_id: None, | |
| recipe: None, | |
| user_recipe_values: None, | |
| conversation, | |
| message_count, | |
| provider_name: None, | |
| model_config: None, | |
| }); | |
| } | |
| } | |
| None | |
| if include_messages { | |
| // Directly load the session by ID, avoiding a full filesystem walk. | |
| let loaded = crate::session::codex::load_codex_session(id).ok()?; | |
| // If load_codex_session returns a struct with working_dir and updated_at, use them. | |
| // Otherwise, you may need to update load_codex_session to return these fields. | |
| let working_dir = loaded.working_dir().clone(); | |
| let updated_at = loaded.updated_at(); | |
| let message_count = loaded.messages().len(); | |
| return Some(Session { | |
| id: id.to_string(), | |
| working_dir, | |
| name: format!( | |
| "Codex Session {}", | |
| id.chars().take(8).collect::<String>() | |
| ), | |
| user_set_name: false, | |
| session_type: SessionType::User, | |
| created_at: updated_at, | |
| updated_at, | |
| extension_data: ExtensionData::default(), | |
| total_tokens: None, | |
| input_tokens: None, | |
| output_tokens: None, | |
| accumulated_total_tokens: None, | |
| accumulated_input_tokens: None, | |
| accumulated_output_tokens: None, | |
| schedule_id: None, | |
| recipe: None, | |
| user_recipe_values: None, | |
| conversation: Some(loaded), | |
| message_count, | |
| provider_name: None, | |
| model_config: None, | |
| }); | |
| } else { | |
| let sessions = crate::session::codex::list_codex_sessions().ok()?; | |
| for (session_id, working_dir, updated_at) in sessions { | |
| if session_id == id { | |
| return Some(Session { | |
| id: session_id.clone(), | |
| working_dir, | |
| name: format!( | |
| "Codex Session {}", | |
| session_id.chars().take(8).collect::<String>() | |
| ), | |
| user_set_name: false, | |
| session_type: SessionType::User, | |
| created_at: updated_at, | |
| updated_at, | |
| extension_data: ExtensionData::default(), | |
| total_tokens: None, | |
| input_tokens: None, | |
| output_tokens: None, | |
| accumulated_total_tokens: None, | |
| accumulated_input_tokens: None, | |
| accumulated_output_tokens: None, | |
| schedule_id: None, | |
| recipe: None, | |
| user_recipe_values: None, | |
| conversation: None, | |
| message_count: 0, | |
| provider_name: None, | |
| model_config: None, | |
| }); | |
| } | |
| } | |
| None | |
| } |
| fn get_home_dir() -> Option<PathBuf> { | ||
| std::env::var("HOME") | ||
| .ok() | ||
| .map(PathBuf::from) | ||
| .or_else(dirs::home_dir) | ||
| } |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The get_home_dir() function is duplicated in both claude_code.rs and codex.rs. Extract this to a shared utility module to avoid duplication.
| for year_entry in std::fs::read_dir(&sessions_dir)? { | ||
| let year_entry = year_entry?; | ||
| let year_path = year_entry.path(); | ||
|
|
||
| if !year_path.is_dir() { | ||
| continue; | ||
| } | ||
|
|
||
| for month_entry in std::fs::read_dir(&year_path)? { | ||
| let month_entry = month_entry?; | ||
| let month_path = month_entry.path(); | ||
|
|
||
| if !month_path.is_dir() { | ||
| continue; | ||
| } | ||
|
|
||
| for day_entry in std::fs::read_dir(&month_path)? { | ||
| let day_entry = day_entry?; | ||
| let day_path = day_entry.path(); | ||
|
|
||
| if !day_path.is_dir() { | ||
| continue; | ||
| } | ||
|
|
||
| for file_entry in std::fs::read_dir(&day_path)? { | ||
| let file_entry = file_entry?; | ||
| let file_path = file_entry.path(); | ||
|
|
||
| if !file_path.is_file() { | ||
| continue; | ||
| } | ||
|
|
||
| let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or(""); | ||
|
|
||
| if !file_name.starts_with("rollout-") || !file_name.ends_with(".jsonl") { | ||
| continue; | ||
| } | ||
|
|
||
| if let Ok((session_id, working_dir, updated_at)) = | ||
| parse_session_metadata(&file_path) | ||
| { | ||
| tracing::debug!( | ||
| "Found Codex session: {} updated at {}", | ||
| session_id, | ||
| updated_at | ||
| ); | ||
| sessions.push((session_id, working_dir, updated_at)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| tracing::debug!("Total Codex sessions found: {}", sessions.len()); | ||
|
|
||
| sessions.sort_by(|a, b| b.2.cmp(&a.2)); | ||
| sessions.truncate(10); | ||
|
|
||
| Ok(sessions) |
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nested directory traversal (3-4 levels deep) in list_codex_sessions() will be slow and I/O intensive. This code is called on every list_sessions() request. Consider caching the results or making this an opt-in background operation.
| // Walk through all directories to find the session file | ||
| for year_entry in std::fs::read_dir(&sessions_dir)? { | ||
| let year_entry = year_entry?; | ||
| let year_path = year_entry.path(); | ||
|
|
||
| if !year_path.is_dir() { | ||
| continue; | ||
| } | ||
|
|
||
| for month_entry in std::fs::read_dir(&year_path)? { | ||
| let month_entry = month_entry?; | ||
| let month_path = month_entry.path(); | ||
|
|
||
| if !month_path.is_dir() { | ||
| continue; | ||
| } | ||
|
|
||
| for day_entry in std::fs::read_dir(&month_path)? { | ||
| let day_entry = day_entry?; | ||
| let day_path = day_entry.path(); | ||
|
|
||
| if !day_path.is_dir() { | ||
| continue; | ||
| } | ||
|
|
||
| for file_entry in std::fs::read_dir(&day_path)? { | ||
| let file_entry = file_entry?; | ||
| let file_path = file_entry.path(); | ||
|
|
||
| if !file_path.is_file() { | ||
| continue; | ||
| } | ||
|
|
||
| let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or(""); | ||
|
|
||
| if file_name.contains(session_id) && file_name.ends_with(".jsonl") { | ||
| return parse_conversation(&file_path); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Err(anyhow::anyhow!("Session not found")) | ||
| } | ||
|
|
Copilot
AI
Nov 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The load_codex_session() function performs a full filesystem walk through potentially hundreds of directories and files every time it's called. Consider optimizing by either: (1) maintaining an index/cache of session file locations, or (2) deriving the file path from session metadata if possible.
| // Walk through all directories to find the session file | |
| for year_entry in std::fs::read_dir(&sessions_dir)? { | |
| let year_entry = year_entry?; | |
| let year_path = year_entry.path(); | |
| if !year_path.is_dir() { | |
| continue; | |
| } | |
| for month_entry in std::fs::read_dir(&year_path)? { | |
| let month_entry = month_entry?; | |
| let month_path = month_entry.path(); | |
| if !month_path.is_dir() { | |
| continue; | |
| } | |
| for day_entry in std::fs::read_dir(&month_path)? { | |
| let day_entry = day_entry?; | |
| let day_path = day_entry.path(); | |
| if !day_path.is_dir() { | |
| continue; | |
| } | |
| for file_entry in std::fs::read_dir(&day_path)? { | |
| let file_entry = file_entry?; | |
| let file_path = file_entry.path(); | |
| if !file_path.is_file() { | |
| continue; | |
| } | |
| let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or(""); | |
| if file_name.contains(session_id) && file_name.ends_with(".jsonl") { | |
| return parse_conversation(&file_path); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Err(anyhow::anyhow!("Session not found")) | |
| } | |
| // Attempt to derive the file path directly from the session ID | |
| if let Some(session_path) = derive_session_file_path(&sessions_dir, session_id) { | |
| if session_path.is_file() { | |
| return parse_conversation(&session_path); | |
| } | |
| } | |
| Err(anyhow::anyhow!("Session not found")) | |
| } | |
| /// Attempts to derive the session file path from the session ID. | |
| /// Assumes session_id format: YYYYMMDD-<unique_id> | |
| fn derive_session_file_path(sessions_dir: &PathBuf, session_id: &str) -> Option<PathBuf> { | |
| // Example session_id: "20240601-abcdef123456" | |
| if session_id.len() < 8 { | |
| return None; | |
| } | |
| let year = &session_id[0..4]; | |
| let month = &session_id[4..6]; | |
| let day = &session_id[6..8]; | |
| // The rest is the unique id | |
| let unique = &session_id[9..]; // skip the dash | |
| // Compose the expected file name | |
| let file_name = format!("{}-{}.jsonl", &session_id[0..8], unique); | |
| let file_path = sessions_dir | |
| .join(year) | |
| .join(month) | |
| .join(day) | |
| .join(file_name); | |
| Some(file_path) | |
| } |
This lets you turn on (off by default) loading of claude and codex sessions. It doesn't import them until you click "resume" in goose (then it will run them as a goose session).