@@ -16,7 +16,8 @@ use std::io::{self, BufRead, BufReader, IsTerminal, Write};
1616use std::path::{Path, PathBuf};
1717use std::process::{Command, Stdio};
1818use std::sync::{Mutex, OnceLock};
19- use std::time::{Duration, SystemTime, UNIX_EPOCH};
19+ use std::thread;
20+ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
2021
2122use anyhow::{Context, Result, bail};
2223use chrono::{DateTime, Utc};
@@ -51,6 +52,12 @@ pub enum Provider {
5152 All,
5253}
5354
55+ const FLOW_CODEX_SESSION_REPORT_PATH_ENV: &str = "FLOW_ZED_CODEX_SESSION_REPORT_PATH";
56+ const CODEX_SESSION_REPORT_PENDING: &str = "__FLOW_ZED_CODEX_SESSION_PENDING__";
57+ const CODEX_SESSION_REPORT_POLL_LIMIT: usize = 8;
58+ const CODEX_SESSION_REPORT_POLL_INTERVAL: Duration = Duration::from_millis(100);
59+ const CODEX_SESSION_REPORT_POLL_TIMEOUT: Duration = Duration::from_secs(15);
60+
5461/// Stored session metadata in .ai/sessions/<provider>/index.json
5562#[derive(Debug, Serialize, Deserialize, Default)]
5663struct SessionIndex {
@@ -3790,6 +3797,138 @@ fn record_direct_codex_launch_event(
37903797 let _ = codex_skill_eval::log_event(&event);
37913798}
37923799
3800+ #[derive(Debug, Default)]
3801+ struct CodexSessionReportBaseline {
3802+ started_at_unix: i64,
3803+ exact_ids: BTreeSet<String>,
3804+ tree_ids: BTreeSet<String>,
3805+ }
3806+
3807+ fn codex_session_report_path_from_env() -> Option<PathBuf> {
3808+ env::var(FLOW_CODEX_SESSION_REPORT_PATH_ENV)
3809+ .ok()
3810+ .map(|value| value.trim().to_string())
3811+ .filter(|value| !value.is_empty())
3812+ .map(PathBuf::from)
3813+ }
3814+
3815+ fn write_codex_session_report(path: &Path, session_id: &str) -> Result<()> {
3816+ if let Some(parent) = path.parent() {
3817+ fs::create_dir_all(parent)
3818+ .with_context(|| format!("failed to create {}", parent.display()))?;
3819+ }
3820+ fs::write(path, format!("{}\n", session_id.trim()))
3821+ .with_context(|| format!("failed to write {}", path.display()))
3822+ }
3823+
3824+ fn clear_pending_codex_session_report(path: &Path) {
3825+ let Ok(current) = fs::read_to_string(path) else {
3826+ return;
3827+ };
3828+ if current.trim() != CODEX_SESSION_REPORT_PENDING {
3829+ return;
3830+ }
3831+ if let Err(err) = fs::remove_file(path) {
3832+ debug!(
3833+ error = %err,
3834+ path = %path.display(),
3835+ "failed to remove pending Codex session report"
3836+ );
3837+ }
3838+ }
3839+
3840+ fn maybe_write_codex_session_report(session_id: &str) {
3841+ let Some(report_path) = codex_session_report_path_from_env() else {
3842+ return;
3843+ };
3844+
3845+ if let Err(err) = write_codex_session_report(&report_path, session_id) {
3846+ debug!(
3847+ error = %err,
3848+ path = %report_path.display(),
3849+ "failed to write Codex session report"
3850+ );
3851+ }
3852+ }
3853+
3854+ fn capture_codex_session_report_baseline(target_path: &Path) -> CodexSessionReportBaseline {
3855+ let exact_ids =
3856+ read_recent_codex_threads_local(target_path, true, CODEX_SESSION_REPORT_POLL_LIMIT, None)
3857+ .unwrap_or_default()
3858+ .into_iter()
3859+ .map(|row| row.id)
3860+ .collect();
3861+ let tree_ids =
3862+ read_recent_codex_threads_local(target_path, false, CODEX_SESSION_REPORT_POLL_LIMIT, None)
3863+ .unwrap_or_default()
3864+ .into_iter()
3865+ .map(|row| row.id)
3866+ .collect();
3867+
3868+ CodexSessionReportBaseline {
3869+ started_at_unix: SystemTime::now()
3870+ .duration_since(UNIX_EPOCH)
3871+ .map(|duration| duration.as_secs() as i64)
3872+ .unwrap_or_default(),
3873+ exact_ids,
3874+ tree_ids,
3875+ }
3876+ }
3877+
3878+ fn find_new_codex_session_report_id(
3879+ target_path: &Path,
3880+ baseline: &CodexSessionReportBaseline,
3881+ ) -> Option<String> {
3882+ let min_updated_at = baseline.started_at_unix.saturating_sub(2);
3883+
3884+ let exact_rows =
3885+ read_recent_codex_threads_local(target_path, true, CODEX_SESSION_REPORT_POLL_LIMIT, None)
3886+ .ok()?;
3887+ if let Some(row) = exact_rows.into_iter().find(|row| {
3888+ row.updated_at >= min_updated_at && !baseline.exact_ids.contains(row.id.as_str())
3889+ }) {
3890+ return Some(row.id);
3891+ }
3892+
3893+ let tree_rows =
3894+ read_recent_codex_threads_local(target_path, false, CODEX_SESSION_REPORT_POLL_LIMIT, None)
3895+ .ok()?;
3896+ tree_rows
3897+ .into_iter()
3898+ .find(|row| {
3899+ row.updated_at >= min_updated_at && !baseline.tree_ids.contains(row.id.as_str())
3900+ })
3901+ .map(|row| row.id)
3902+ }
3903+
3904+ fn start_new_codex_session_reporter(report_path: PathBuf, target_path: PathBuf) {
3905+ let baseline = capture_codex_session_report_baseline(&target_path);
3906+ thread::spawn(move || {
3907+ let started_at = Instant::now();
3908+ while started_at.elapsed() < CODEX_SESSION_REPORT_POLL_TIMEOUT {
3909+ if let Some(session_id) = find_new_codex_session_report_id(&target_path, &baseline) {
3910+ if let Err(err) = write_codex_session_report(&report_path, &session_id) {
3911+ debug!(
3912+ error = %err,
3913+ path = %report_path.display(),
3914+ "failed to write new Codex session report"
3915+ );
3916+ }
3917+ return;
3918+ }
3919+
3920+ thread::sleep(CODEX_SESSION_REPORT_POLL_INTERVAL);
3921+ }
3922+
3923+ clear_pending_codex_session_report(&report_path);
3924+ debug!(
3925+ path = %report_path.display(),
3926+ target_path = %target_path.display(),
3927+ "timed out while waiting for a new Codex session id to appear"
3928+ );
3929+ });
3930+ }
3931+
37933932fn launch_session_for_target(
37943933 session_id: &str,
37953934 provider: Provider,
@@ -3837,6 +3976,7 @@ fn launch_session_for_target(
38373976 if let Some(prompt) = prompt.map(str::trim).filter(|value| !value.is_empty()) {
38383977 command.arg(prompt);
38393978 }
3979+ maybe_write_codex_session_report(session_id);
38403980 let status = command.status().with_context(|| "failed to launch codex")?;
38413981 if status.success() && direct_log {
38423982 record_direct_codex_launch_event(
@@ -4395,6 +4535,12 @@ fn launch_codex_continue_last_for_target(target_path: Option<&Path>) -> Result<b
43954535 let workdir = target_path
43964536 .map(Path::to_path_buf)
43974537 .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
4538+ if let Some(session_id) = read_recent_codex_threads(&workdir, true, 1, None)?
4539+ .first()
4540+ .map(|row| row.id.clone())
4541+ {
4542+ maybe_write_codex_session_report(&session_id);
4543+ }
43984544 let trace = new_codex_session_trace("continue_last_session");
43994545 let mut command = Command::new(configured_codex_bin_for_workdir(&workdir));
44004546 command.arg("resume");
@@ -4423,6 +4569,26 @@ fn launch_codex_continue_last_for_target(target_path: Option<&Path>) -> Result<b
44234569 Ok(status.success())
44244570}
44254571
4572+ fn should_fast_path_codex_connect(query_text: &str, exact_cwd: bool, json_output: bool) -> bool {
4573+ query_text.trim().is_empty() && exact_cwd && !json_output
4574+ }
4575+
4576+ fn record_codex_connect_activity(
4577+ summary: &str,
4578+ route: &str,
4579+ target_path: &Path,
4580+ launch_path: &Path,
4581+ session_id: Option<&str>,
4582+ ) {
4583+ let mut connect_event = activity_log::ActivityEvent::done("codex.connect", summary);
4584+ connect_event.route = Some(route.to_string());
4585+ connect_event.target_path = Some(target_path.display().to_string());
4586+ connect_event.launch_path = Some(launch_path.display().to_string());
4587+ connect_event.session_id = session_id.map(str::to_string);
4588+ connect_event.source = Some("codex-connect".to_string());
4589+ let _ = activity_log::append_daily_event(connect_event);
4590+ }
4591+
44264592fn provider_name(provider: Provider) -> &'static str {
44274593 match provider {
44284594 Provider::Claude => "claude",
@@ -4725,7 +4891,25 @@ fn new_session_for_target(
47254891 if let Some(prompt) = prompt.map(str::trim).filter(|value| !value.is_empty()) {
47264892 command.arg(prompt);
47274893 }
4894+ let report_path = codex_session_report_path_from_env();
4895+ if let Some(report_path) = report_path.clone() {
4896+ if let Err(err) =
4897+ write_codex_session_report(&report_path, CODEX_SESSION_REPORT_PENDING)
4898+ {
4899+ debug!(
4900+ error = %err,
4901+ path = %report_path.display(),
4902+ "failed to clear stale Codex session report before new launch"
4903+ );
4904+ }
4905+ start_new_codex_session_reporter(report_path, workdir.clone());
4906+ }
47284907 let status = command.status().with_context(|| "failed to launch codex")?;
4908+ if !status.success()
4909+ && let Some(report_path) = report_path.as_deref()
4910+ {
4911+ clear_pending_codex_session_report(report_path);
4912+ }
47294913 if status.success() && direct_log {
47304914 record_direct_codex_launch_event(
47314915 "new",
@@ -6848,6 +7032,19 @@ fn connect_codex_session(
68487032
68497033 let target_path = resolve_codex_connect_target_path(path)?;
68507034 let query_text = query.join(" ").trim().to_string();
7035+ if should_fast_path_codex_connect(&query_text, exact_cwd, json_output) {
7036+ ensure_provider_tty(Provider::Codex, "connect")?;
7037+ if launch_codex_continue_last_for_target(Some(&target_path))? {
7038+ record_codex_connect_activity(
7039+ "resume latest recent session",
7040+ "latest",
7041+ &target_path,
7042+ &target_path,
7043+ None,
7044+ );
7045+ return Ok(());
7046+ }
7047+ }
68517048 let normalized_query = query_text.to_ascii_lowercase();
68527049 let resolved = if query_text.is_empty() {
68537050 read_recent_codex_threads(&target_path, exact_cwd, 1, None)?
@@ -6915,19 +7112,19 @@ fn connect_codex_session(
69157112 } else {
69167113 query_text.clone()
69177114 };
6918- let mut connect_event = activity_log::ActivityEvent::done("codex.connect", connect_summary);
6919- connect_event.route = Some(if query_text.is_empty() {
6920- "latest".to_string()
6921- } else {
6922- "query".to_string()
6923- });
6924- connect_event.target_path = Some(target_path.display().to_string());
6925- connect_event.launch_path = Some(row.cwd.clone());
6926- connect_event.session_id = Some(row.id.clone());
6927- connect_event.source = Some("codex-connect".to_string());
6928- let _ = activity_log::append_daily_event(connect_event);
6929-
69307115 let launch_path = PathBuf::from(&row.cwd);
7116+ record_codex_connect_activity(
7117+ &connect_summary,
7118+ if query_text.is_empty() {
7119+ "latest"
7120+ } else {
7121+ "query"
7122+ },
7123+ &target_path,
7124+ &launch_path,
7125+ Some(&row.id),
7126+ );
7127+
69317128 println!(
69327129 "Resuming session {} from {}...",
69337130 &row.id[..8.min(row.id.len())],
@@ -15218,6 +15415,19 @@ mod tests {
1521815415 );
1521915416 }
1522015417
15418+ #[test]
15419+ fn fast_path_codex_connect_only_for_empty_exact_non_json() {
15420+ assert!(should_fast_path_codex_connect("", true, false));
15421+ assert!(should_fast_path_codex_connect(" ", true, false));
15422+ assert!(!should_fast_path_codex_connect(
15423+ "resume latest",
15424+ true,
15425+ false
15426+ ));
15427+ assert!(!should_fast_path_codex_connect("", false, false));
15428+ assert!(!should_fast_path_codex_connect("", true, true));
15429+ }
15430+
1522115431 #[test]
1522215432 fn select_codex_state_db_path_prefers_highest_version() {
1522315433 let root = tempdir().expect("tempdir");
0 commit comments