Discard archived session checkpoints
This commit is contained in:
58
src/app.rs
58
src/app.rs
@@ -1757,11 +1757,19 @@ impl App {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
for session_id in &checkpoint_ids {
|
||||||
|
if let Err(error) =
|
||||||
|
discard_session_checkpoint_files(&kv_cache_path(), *session_id)
|
||||||
|
{
|
||||||
|
self.error = Some(error);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.finish_cache_change();
|
||||||
if let Some(database) = &mut self.database {
|
if let Some(database) = &mut self.database {
|
||||||
match database.delete_project(project_id) {
|
match database.delete_project(project_id) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
for session_id in checkpoint_ids {
|
for session_id in checkpoint_ids {
|
||||||
let _ = fs::remove_file(session_checkpoint_path(session_id));
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
self.background_chats.remove(&session_id);
|
self.background_chats.remove(&session_id);
|
||||||
}
|
}
|
||||||
@@ -1902,22 +1910,33 @@ impl App {
|
|||||||
self.error =
|
self.error =
|
||||||
Some("Stop the active generation before rebuilding context.".into());
|
Some("Stop the active generation before rebuilding context.".into());
|
||||||
} else {
|
} else {
|
||||||
match fs::remove_file(session_checkpoint_path(session_id)) {
|
match discard_session_checkpoint_files(&kv_cache_path(), session_id) {
|
||||||
Ok(()) => {
|
Ok(_) => {
|
||||||
self.error = None;
|
self.error = None;
|
||||||
self.finish_cache_change();
|
self.finish_cache_change();
|
||||||
}
|
}
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
|
||||||
self.error = None;
|
|
||||||
}
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
self.error = Some(format!("Could not discard the checkpoint: {error}"));
|
self.error = Some(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::SetSessionState(session_id, state) => {
|
Message::SetSessionState(session_id, state) => {
|
||||||
self.session_menu = None;
|
self.session_menu = None;
|
||||||
|
if state == SessionState::Archived {
|
||||||
|
if self.session_is_active(session_id) {
|
||||||
|
self.error =
|
||||||
|
Some("Stop the active generation before archiving its session.".into());
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
if let Err(error) =
|
||||||
|
discard_session_checkpoint_files(&kv_cache_path(), session_id)
|
||||||
|
{
|
||||||
|
self.error = Some(error);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.finish_cache_change();
|
||||||
|
}
|
||||||
if let Some(database) = &mut self.database {
|
if let Some(database) = &mut self.database {
|
||||||
match database.set_session_state(session_id, state) {
|
match database.set_session_state(session_id, state) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
@@ -2047,13 +2066,16 @@ impl App {
|
|||||||
Some("Stop the active generation before deleting its session.".into());
|
Some("Stop the active generation before deleting its session.".into());
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
if let Err(error) = discard_session_checkpoint_files(&kv_cache_path(), session_id) {
|
||||||
|
self.error = Some(error);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.finish_cache_change();
|
||||||
if let Some(database) = &mut self.database {
|
if let Some(database) = &mut self.database {
|
||||||
match database.delete_session(session_id) {
|
match database.delete_session(session_id) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let _ = fs::remove_file(session_checkpoint_path(session_id));
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
self.background_chats.remove(&session_id);
|
self.background_chats.remove(&session_id);
|
||||||
self.finish_cache_change();
|
|
||||||
if self.session_menu == Some(session_id) {
|
if self.session_menu == Some(session_id) {
|
||||||
self.session_menu = None;
|
self.session_menu = None;
|
||||||
}
|
}
|
||||||
@@ -2723,6 +2745,24 @@ fn session_checkpoint_path(session_id: i32) -> PathBuf {
|
|||||||
kv_cache_path().join(format!("{session_id}.bin"))
|
kv_cache_path().join(format!("{session_id}.bin"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn discard_session_checkpoint_files(directory: &Path, session_id: i32) -> Result<bool, String> {
|
||||||
|
let mut removed = false;
|
||||||
|
for extension in ["bin", "tmp", "compacting"] {
|
||||||
|
let path = directory.join(format!("{session_id}.{extension}"));
|
||||||
|
match fs::remove_file(&path) {
|
||||||
|
Ok(()) => removed = true,
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(error) => {
|
||||||
|
return Err(format!(
|
||||||
|
"Could not discard session checkpoint {}: {error}",
|
||||||
|
path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(removed)
|
||||||
|
}
|
||||||
|
|
||||||
fn session_compaction_checkpoint_path(session_id: i32) -> PathBuf {
|
fn session_compaction_checkpoint_path(session_id: i32) -> PathBuf {
|
||||||
kv_cache_path().join(format!("{session_id}.compacting"))
|
kv_cache_path().join(format!("{session_id}.compacting"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -613,7 +613,13 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let Some(service) = &self.generation_service else {
|
let archived_session = self
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.flat_map(|project| &project.sessions)
|
||||||
|
.find(|session| session.id == session_id)
|
||||||
|
.is_some_and(|session| session.state() == SessionState::Archived);
|
||||||
|
let Some(service) = self.generation_service.clone() else {
|
||||||
self.error = Some("The model runtime is unavailable.".into());
|
self.error = Some("The model runtime is unavailable.".into());
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -633,6 +639,13 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if archived_session {
|
||||||
|
self.reload_projects();
|
||||||
|
self.context_notice = Some(
|
||||||
|
"Rebuilding context: the session was archived and its checkpoint was discarded."
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
let user_id = saved[saved.len() - 2].id;
|
let user_id = saved[saved.len() - 2].id;
|
||||||
self.active_turn
|
self.active_turn
|
||||||
.get_or_insert_with(TurnSummary::new)
|
.get_or_insert_with(TurnSummary::new)
|
||||||
@@ -1643,6 +1656,12 @@ impl App {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
let archived_session = self
|
||||||
|
.projects
|
||||||
|
.iter()
|
||||||
|
.flat_map(|project| &project.sessions)
|
||||||
|
.find(|session| session.id == session_id)
|
||||||
|
.is_some_and(|session| session.state() == SessionState::Archived);
|
||||||
let tail_start = message_ids.get(compacted.tail_start).copied();
|
let tail_start = message_ids.get(compacted.tail_start).copied();
|
||||||
let messages = self
|
let messages = self
|
||||||
.database
|
.database
|
||||||
@@ -1659,7 +1678,9 @@ impl App {
|
|||||||
.map_err(|error| format!("Could not save compacted conversation: {error}"))?;
|
.map_err(|error| format!("Could not save compacted conversation: {error}"))?;
|
||||||
self.conversation
|
self.conversation
|
||||||
.extend(messages.into_iter().map(ChatMessage::from));
|
.extend(messages.into_iter().map(ChatMessage::from));
|
||||||
if let Some(session) = self
|
if archived_session {
|
||||||
|
self.reload_projects();
|
||||||
|
} else if let Some(session) = self
|
||||||
.projects
|
.projects
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.flat_map(|project| &mut project.sessions)
|
.flat_map(|project| &mut project.sessions)
|
||||||
|
|||||||
@@ -515,6 +515,25 @@ mod tests {
|
|||||||
std::fs::remove_dir_all(directory).unwrap();
|
std::fs::remove_dir_all(directory).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discarding_a_session_removes_every_checkpoint_stage() {
|
||||||
|
let directory =
|
||||||
|
std::env::temp_dir().join(format!("ds4-server-discard-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&directory).unwrap();
|
||||||
|
for name in ["4.bin", "4.tmp", "4.compacting", "5.bin"] {
|
||||||
|
std::fs::write(directory.join(name), b"payload").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(super::super::discard_session_checkpoint_files(&directory, 4).unwrap());
|
||||||
|
assert!(!directory.join("4.bin").exists());
|
||||||
|
assert!(!directory.join("4.tmp").exists());
|
||||||
|
assert!(!directory.join("4.compacting").exists());
|
||||||
|
assert!(directory.join("5.bin").exists());
|
||||||
|
assert!(!super::super::discard_session_checkpoint_files(&directory, 4).unwrap());
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(directory).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn git_state_lists_and_switches_local_branches() {
|
fn git_state_lists_and_switches_local_branches() {
|
||||||
let nonce = SystemTime::now()
|
let nonce = SystemTime::now()
|
||||||
|
|||||||
@@ -448,6 +448,7 @@ impl Database {
|
|||||||
) -> Result<Vec<StoredMessage>, String> {
|
) -> Result<Vec<StoredMessage>, String> {
|
||||||
self.connection
|
self.connection
|
||||||
.transaction(|connection| {
|
.transaction(|connection| {
|
||||||
|
reactivate_archived_session(connection, session_id)?;
|
||||||
touch_session(connection, session_id)?;
|
touch_session(connection, session_id)?;
|
||||||
let mut stored = Vec::with_capacity(system_messages.len() + 2);
|
let mut stored = Vec::with_capacity(system_messages.len() + 2);
|
||||||
for content in system_messages {
|
for content in system_messages {
|
||||||
@@ -654,6 +655,7 @@ impl Database {
|
|||||||
.map_err(|_| "Context limit is too large to save".to_owned())?;
|
.map_err(|_| "Context limit is too large to save".to_owned())?;
|
||||||
self.connection
|
self.connection
|
||||||
.transaction(|connection| {
|
.transaction(|connection| {
|
||||||
|
reactivate_archived_session(connection, session_id)?;
|
||||||
diesel::update(sessions::table.find(session_id))
|
diesel::update(sessions::table.find(session_id))
|
||||||
.set((
|
.set((
|
||||||
sessions::compacted_summary.eq(Some(summary)),
|
sessions::compacted_summary.eq(Some(summary)),
|
||||||
@@ -715,6 +717,20 @@ fn touch_session(
|
|||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reactivate_archived_session(
|
||||||
|
connection: &mut SqliteConnection,
|
||||||
|
session_id: i32,
|
||||||
|
) -> Result<(), diesel::result::Error> {
|
||||||
|
diesel::update(
|
||||||
|
sessions::table
|
||||||
|
.find(session_id)
|
||||||
|
.filter(sessions::state.eq(SessionState::Archived.as_id())),
|
||||||
|
)
|
||||||
|
.set(sessions::state.eq(SessionState::Normal.as_id()))
|
||||||
|
.execute(connection)
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -827,6 +843,44 @@ mod tests {
|
|||||||
fs::remove_file(path).unwrap();
|
fs::remove_file(path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_work_reactivates_an_archived_session() {
|
||||||
|
let id = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let path = std::env::temp_dir().join(format!("ds4-reactivate-{id}.sqlite3"));
|
||||||
|
let mut database = Database::open(&path).unwrap();
|
||||||
|
let project = database
|
||||||
|
.create_project("DS4", "/tmp/ds4-reactivate")
|
||||||
|
.unwrap();
|
||||||
|
let session = database.create_session(project.id, "Archived").unwrap();
|
||||||
|
database
|
||||||
|
.set_session_state(session.id, SessionState::Archived)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
database
|
||||||
|
.start_chat_turn(session.id, "Resume", None, &[], false)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
database.load_projects().unwrap()[0].sessions[0].state(),
|
||||||
|
SessionState::Normal
|
||||||
|
);
|
||||||
|
database
|
||||||
|
.set_session_state(session.id, SessionState::Archived)
|
||||||
|
.unwrap();
|
||||||
|
database
|
||||||
|
.record_compaction(session.id, "Summary", None, None, 100, 1_000)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
database.load_projects().unwrap()[0].sessions[0].state(),
|
||||||
|
SessionState::Normal
|
||||||
|
);
|
||||||
|
drop(database);
|
||||||
|
fs::remove_file(path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a2ui_dismissal_persists_a_fresh_surface_boundary() {
|
fn a2ui_dismissal_persists_a_fresh_surface_boundary() {
|
||||||
let id = SystemTime::now()
|
let id = SystemTime::now()
|
||||||
|
|||||||
@@ -962,15 +962,6 @@ impl Generator {
|
|||||||
matches: impl Fn([u8; 32]) -> bool,
|
matches: impl Fn([u8; 32]) -> bool,
|
||||||
) -> Result<CheckpointSelection, String> {
|
) -> Result<CheckpointSelection, String> {
|
||||||
let resident_hit = self.activate_resident(checkpoint.to_owned())?;
|
let resident_hit = self.activate_resident(checkpoint.to_owned())?;
|
||||||
if resident_hit && matches(self.executor.checkpoint_tag()) {
|
|
||||||
self.checkpoint = Some(checkpoint.to_owned());
|
|
||||||
self.metrics.kv_lookup(KvLookup::MemoryHit);
|
|
||||||
return Ok(CheckpointSelection {
|
|
||||||
found: true,
|
|
||||||
incompatible: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if self.checkpoint.as_deref() == Some(checkpoint) {
|
|
||||||
if !checkpoint.is_file() {
|
if !checkpoint.is_file() {
|
||||||
self.executor.reset()?;
|
self.executor.reset()?;
|
||||||
self.checkpoint = None;
|
self.checkpoint = None;
|
||||||
@@ -980,6 +971,15 @@ impl Generator {
|
|||||||
incompatible: false,
|
incompatible: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if resident_hit && matches(self.executor.checkpoint_tag()) {
|
||||||
|
self.checkpoint = Some(checkpoint.to_owned());
|
||||||
|
self.metrics.kv_lookup(KvLookup::MemoryHit);
|
||||||
|
return Ok(CheckpointSelection {
|
||||||
|
found: true,
|
||||||
|
incompatible: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.checkpoint.as_deref() == Some(checkpoint) {
|
||||||
let found = matches(self.executor.checkpoint_tag());
|
let found = matches(self.executor.checkpoint_tag());
|
||||||
self.metrics.kv_lookup(if found {
|
self.metrics.kv_lookup(if found {
|
||||||
KvLookup::MemoryHit
|
KvLookup::MemoryHit
|
||||||
|
|||||||
Reference in New Issue
Block a user