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

@@ -20,13 +20,14 @@ pub(super) struct TitleRequest {
#[cfg(target_os = "macos")]
pub(super) enum PendingContinuation {
None,
User(String),
Tool(String),
}
#[cfg(target_os = "macos")]
pub(super) struct CompactionRequest {
active: ActiveGeneration,
pub(super) active: ActiveGeneration,
pending: PendingContinuation,
}
@@ -92,6 +93,16 @@ impl App {
return;
}
#[cfg(target_os = "macos")]
if prompt == "/compact" {
self.composer.clear();
if let Err(error) =
self.start_compaction(PendingContinuation::None, "manual /compact request")
{
self.error = Some(error);
}
return;
}
#[cfg(target_os = "macos")]
if !std::mem::take(&mut self.skip_compaction_once)
&& crate::compaction::should_compact(self.context_used, self.context_limit)
{
@@ -466,7 +477,7 @@ impl App {
}
#[cfg(target_os = "macos")]
fn start_compaction(
pub(super) fn start_compaction(
&mut self,
pending: PendingContinuation,
reason: &str,
@@ -506,6 +517,10 @@ impl App {
effective.turn,
messages,
reason,
session_compaction_checkpoint_path(
self.selected_session
.ok_or_else(|| "The active session is unavailable.".to_owned())?,
),
idle_timeout,
)?;
self.active_compaction = Some(CompactionRequest { active, pending });
@@ -537,6 +552,7 @@ impl App {
match result {
Ok(compacted) => {
if let Err(error) = self.apply_compaction(&compacted) {
let _ = fs::remove_file(&compacted.checkpoint);
self.generating = false;
self.activity = None;
self.error = Some(error);
@@ -546,6 +562,7 @@ impl App {
self.generating = false;
self.activity = None;
match request.pending {
PendingContinuation::None => {}
PendingContinuation::User(prompt) => {
self.composer = prompt;
self.skip_compaction_once = true;
@@ -595,7 +612,7 @@ impl App {
let session_id = self
.selected_session
.ok_or_else(|| "The active session is unavailable.".to_owned())?;
let tail = compacted
let mut tail = compacted
.tail
.iter()
.map(|message| crate::database::MessageDraft {
@@ -606,11 +623,32 @@ impl App {
content: message.content.clone(),
})
.collect::<Vec<_>>();
if let Some((tools_session, tools)) = &self.agent_tools
&& *tools_session == session_id
&& let Some(observation) = tools
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.compaction_observation()
{
tail.push(crate::database::MessageDraft {
user: false,
tool: true,
reasoning: None,
reasoning_complete: true,
content: observation,
});
}
let messages = self
.database
.as_mut()
.ok_or_else(|| "The project database is unavailable.".to_owned())?
.replace_with_compacted_transcript(session_id, &compacted.summary, &tail)
.replace_with_compacted_transcript(
session_id,
&compacted.summary,
&tail,
compacted.context_tokens,
self.context_limit,
)
.map_err(|error| format!("Could not save compacted conversation: {error}"))?;
self.conversation = messages.into_iter().map(ChatMessage::from).collect();
if let Some(session) = self
@@ -620,8 +658,16 @@ impl App {
.find(|session| session.id == session_id)
{
session.compacted_summary = Some(compacted.summary.clone());
session.context_used = compacted.context_tokens as i32;
session.context_limit = self.context_limit as i32;
session.last_tokens_per_second = None;
}
let _ = fs::remove_file(session_checkpoint_path(session_id));
let final_checkpoint = session_checkpoint_path(session_id);
fs::rename(&compacted.checkpoint, &final_checkpoint).map_err(|error| {
format!(
"The compacted conversation was saved, but its checkpoint could not be promoted: {error}. It will rebuild on next use."
)
})?;
Ok(())
}