Skip to content

Commit 83a5f89

Browse files
committed
Report Codex session ids for Zed terminal restore
1 parent 860a102 commit 83a5f89

1 file changed

Lines changed: 223 additions & 13 deletions

File tree

src/ai.rs

Lines changed: 223 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ use std::io::{self, BufRead, BufReader, IsTerminal, Write};
1616
use std::path::{Path, PathBuf};
1717
use std::process::{Command, Stdio};
1818
use 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

2122
use anyhow::{Context, Result, bail};
2223
use 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)]
5663
struct 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+
37933932
fn 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+
44264592
fn 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

Comments
 (0)