From ea43cb507d1d1cfbf0ad4d14c42610c53844e632 Mon Sep 17 00:00:00 2001 From: Subin An Date: Sat, 25 Apr 2026 09:52:23 +0900 Subject: [PATCH 1/2] fix(cache): preserve prior cache entry for in-flight agents on TUI exit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.10.1 introduced streaming background scans: the TUI opens on cached sessions immediately and stale agents stream their results in. `cache::write_cache` runs at TUI exit to persist the merged result. But if the user exits before a worker has reported in, `write_cache` would re-emit that agent with an empty session list and the current `mtime` of its data sources — making the cache look "fresh + empty" on the next launch and hiding the agent's sessions until a new file write changed the mtime again. Fix: pass the still-scanning agent set into `write_cache`. For each agent in that set, carry its prior cache entry (mtime + sessions) verbatim from the on-disk file, so an interrupted scan doesn't overwrite known-good cache state with `(now, [])`. Behavior of completed scans is unchanged. --- src/cache.rs | 47 ++++++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 4 +++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/cache.rs b/src/cache.rs index 01b681a..eb9e6d7 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -181,7 +181,13 @@ pub fn load_cache() -> (Vec, Vec) { } /// Write all sessions to cache, grouped by agent. -pub fn write_cache(sessions: &[Session]) { +/// +/// `skip_agents` lists agents whose background scan did not complete this +/// run (e.g. the user exited the TUI before the worker finished); for those +/// we preserve the prior cache entry verbatim so we don't accidentally +/// persist an empty session list with a fresh `mtime`, which would mark the +/// agent "fresh" on the next launch and hide its sessions. +pub fn write_cache(sessions: &[Session], skip_agents: &std::collections::HashSet) { let path = cache_path(); if let Some(parent) = path.parent() { let _ = fs::create_dir_all(parent); @@ -192,10 +198,35 @@ pub fn write_cache(sessions: &[Session]) { let plugins = plugin::all_plugins(); let mut agents: HashMap = HashMap::new(); + // Carry over prior cache entries for in-flight agents. + if !skip_agents.is_empty() { + if let Ok(content) = fs::read_to_string(&path) { + if let Ok(prior) = serde_json::from_str::(&content) { + if prior.version == CACHE_VERSION { + for skip in skip_agents { + let key = agent_to_str(*skip).to_string(); + if let Some(entry) = prior.agents.get(&key) { + agents.insert( + key, + AgentCache { + mtime: entry.mtime, + sessions: entry.sessions.iter().map(clone_cached).collect(), + }, + ); + } + } + } + } + } + } + for p in &plugins { if !installed.contains(&p.agent()) { continue; } + if skip_agents.contains(&p.agent()) { + continue; + } let key = agent_to_str(p.agent()).to_string(); let agent_sessions: Vec = sessions .iter() @@ -225,6 +256,20 @@ pub fn write_cache(sessions: &[Session]) { } } +fn clone_cached(c: &CachedSession) -> CachedSession { + CachedSession { + agent: c.agent.clone(), + session_id: c.session_id.clone(), + project_name: c.project_name.clone(), + project_path: c.project_path.clone(), + summaries: c.summaries.clone(), + timestamp: c.timestamp, + git_branch: c.git_branch.clone(), + worktree: c.worktree.clone(), + recap: c.recap.clone(), + } +} + /// One agent's scan result, streamed back from a worker thread. pub struct ScanResult { pub agent: Agent, diff --git a/src/main.rs b/src/main.rs index 5306e89..2e3f125 100644 --- a/src/main.rs +++ b/src/main.rs @@ -256,7 +256,9 @@ fn main() -> anyhow::Result<()> { let result = app.run()?; // Persist whatever sessions accumulated during the TUI lifetime so // the next launch reflects all scans that completed before exit. - cache::write_cache(&app.sessions); + // Agents still scanning at exit keep their prior cache entry so we + // don't accidentally persist "empty + fresh-mtime" for them. + cache::write_cache(&app.sessions, &app.scanning_agents); result }; From d7840bddd916c142046049b3896ba8b292c08088 Mon Sep 17 00:00:00 2001 From: Subin An Date: Sat, 25 Apr 2026 09:54:20 +0900 Subject: [PATCH 2/2] chore: release v0.10.2 Bumps version + CHANGELOG for the cache-write-race fix in this PR. Tag and release happen after merge. --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65863cc..0dffc00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.10.2] - 2026-04-25 + +### Fixed + +- **Cache write race on early TUI exit (regression in 0.10.1)** — v0.10.1 introduced streaming background scans; if the user exited the TUI before a worker finished, `cache::write_cache` persisted that agent as `(empty session list, fresh mtime)`, hiding its sessions on the next launch until file mtime changed again. `write_cache` now takes the still-scanning agent set and carries the prior cache entry verbatim for those agents — only completed scans replace cache state. + ## [0.10.1] - 2026-04-25 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index a7b3a2f..5458ac1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agf" -version = "0.10.1" +version = "0.10.2" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index a552363..bc0839e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agf" -version = "0.10.1" +version = "0.10.2" edition = "2021" description = "AI Agent Session Finder TUI — find, resume, and manage Claude Code, Codex, OpenCode, Gemini, Kiro, pi, and Cursor CLI sessions" license = "MIT"