Make compaction transitions recoverable
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -196,7 +196,14 @@ mod tests {
|
||||
let directory =
|
||||
std::env::temp_dir().join(format!("ds4-server-sweep-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&directory).unwrap();
|
||||
for name in ["1.bin", "2.bin", "notes.bin", "7.bin"] {
|
||||
for name in [
|
||||
"1.bin",
|
||||
"2.bin",
|
||||
"notes.bin",
|
||||
"7.bin",
|
||||
"1.compacting",
|
||||
"2.tmp",
|
||||
] {
|
||||
std::fs::write(directory.join(name), b"payload").unwrap();
|
||||
}
|
||||
let projects = vec![ProjectWithSessions {
|
||||
@@ -213,6 +220,8 @@ mod tests {
|
||||
// Not a session checkpoint, so not ours to delete.
|
||||
assert!(directory.join("notes.bin").exists());
|
||||
assert!(!directory.join("7.bin").exists());
|
||||
assert!(!directory.join("1.compacting").exists());
|
||||
assert!(!directory.join("2.tmp").exists());
|
||||
// Nothing left to sweep on the next pass.
|
||||
assert!(!super::super::sweep_orphan_checkpoints(
|
||||
&directory, &projects
|
||||
|
||||
@@ -446,6 +446,10 @@ impl App {
|
||||
.on_press(Message::StartRenameSession(session.id)),
|
||||
menu_action(ICON_SPARK, "Retitle with AI")
|
||||
.on_press(Message::RetitleSession(session.id)),
|
||||
menu_action(ICON_SPARK, "Compact context")
|
||||
.on_press(Message::CompactSession(session.id)),
|
||||
menu_action(ICON_ARCHIVE, "Rebuild context on next use")
|
||||
.on_press(Message::RebuildSessionContext(session.id)),
|
||||
]
|
||||
.spacing(4);
|
||||
actions = match state {
|
||||
|
||||
Reference in New Issue
Block a user