Make compaction transitions recoverable

This commit is contained in:
Georg Bauer
2026-07-26 10:56:19 +02:00
parent 6aa45b2cf0
commit 2a14b93335
13 changed files with 329 additions and 26 deletions

View File

@@ -212,6 +212,8 @@ pub(crate) enum Message {
SessionTitleChanged(String),
ConfirmRenameSession,
RetitleSession(i32),
CompactSession(i32),
RebuildSessionContext(i32),
SetSessionState(i32, SessionState),
ToggleArchivedSessions(i32),
ShowChat,
@@ -773,6 +775,10 @@ impl App {
if let Some(active) = &self.active_tools {
active.cancel.store(true, Ordering::Relaxed);
}
#[cfg(target_os = "macos")]
if let Some(compaction) = &self.active_compaction {
compaction.active.cancel.store(true, Ordering::Relaxed);
}
}
Message::GenerationTick => {
#[cfg(target_os = "macos")]
@@ -924,6 +930,44 @@ impl App {
self.error = Some("Local Metal generation requires macOS.".into());
}
}
Message::CompactSession(session_id) => {
self.session_menu = None;
if self.generating || self.selected_session != Some(session_id) {
self.error = Some("Open an idle session before compacting it.".into());
} else {
#[cfg(target_os = "macos")]
if let Err(error) = self.start_compaction(
generation::PendingContinuation::None,
"manual compact action",
) {
self.error = Some(error);
}
#[cfg(not(target_os = "macos"))]
{
self.error = Some("Local Metal generation requires macOS.".into());
}
}
}
Message::RebuildSessionContext(session_id) => {
self.session_menu = None;
if self.generating && self.selected_session == Some(session_id) {
self.error =
Some("Stop the active generation before rebuilding context.".into());
} else {
match fs::remove_file(session_checkpoint_path(session_id)) {
Ok(()) => {
self.error = None;
self.finish_cache_change();
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
self.error = None;
}
Err(error) => {
self.error = Some(format!("Could not discard the checkpoint: {error}"));
}
}
}
}
Message::SetSessionState(session_id, state) => {
self.session_menu = None;
if let Some(database) = &mut self.database {
@@ -1402,6 +1446,13 @@ fn sweep_orphan_checkpoints(directory: &Path, projects: &[ProjectWithSessions])
let mut removed = false;
for file in files.flatten() {
let path = file.path();
if path
.extension()
.is_some_and(|value| value == "compacting" || value == "tmp")
{
removed |= fs::remove_file(&path).is_ok();
continue;
}
if path.extension().is_none_or(|value| value != "bin") {
continue;
}
@@ -1427,6 +1478,10 @@ fn session_checkpoint_path(session_id: i32) -> PathBuf {
kv_cache_path().join(format!("{session_id}.bin"))
}
fn session_compaction_checkpoint_path(session_id: i32) -> PathBuf {
kv_cache_path().join(format!("{session_id}.compacting"))
}
/// Content-addressed KV cache for turns that belong to no stored session: the
/// HTTP endpoint and the app's own one-shot requests share it.
fn transient_cache_path() -> PathBuf {