feat(session-anchors): implement S-2 session anchor persistence and auto-anchoring

This commit is contained in:
Hibryda 2026-03-11 02:43:06 +01:00
parent 8f4faaafa3
commit a9e94fc154
7 changed files with 616 additions and 1 deletions

View file

@ -492,6 +492,49 @@ fn session_metrics_load(
state.session_db.load_session_metrics(&project_id, limit)
}
// --- Session anchor commands ---
#[tauri::command]
fn session_anchors_save(
state: State<'_, AppState>,
anchors: Vec<session::SessionAnchorRecord>,
) -> Result<(), String> {
state.session_db.save_session_anchors(&anchors)
}
#[tauri::command]
fn session_anchors_load(
state: State<'_, AppState>,
project_id: String,
) -> Result<Vec<session::SessionAnchorRecord>, String> {
state.session_db.load_session_anchors(&project_id)
}
#[tauri::command]
fn session_anchor_delete(
state: State<'_, AppState>,
id: String,
) -> Result<(), String> {
state.session_db.delete_session_anchor(&id)
}
#[tauri::command]
fn session_anchors_clear(
state: State<'_, AppState>,
project_id: String,
) -> Result<(), String> {
state.session_db.delete_project_anchors(&project_id)
}
#[tauri::command]
fn session_anchor_update_type(
state: State<'_, AppState>,
id: String,
anchor_type: String,
) -> Result<(), String> {
state.session_db.update_anchor_type(&id, &anchor_type)
}
// --- File browser commands (Files tab) ---
#[derive(serde::Serialize)]
@ -810,6 +853,11 @@ pub fn run() {
project_agent_state_load,
session_metric_save,
session_metrics_load,
session_anchors_save,
session_anchors_load,
session_anchor_delete,
session_anchors_clear,
session_anchor_update_type,
cli_get_group,
pick_directory,
open_url,

View file

@ -171,7 +171,20 @@ impl SessionDb {
error_message TEXT
);
CREATE INDEX IF NOT EXISTS idx_session_metrics_project
ON session_metrics(project_id);"
ON session_metrics(project_id);
CREATE TABLE IF NOT EXISTS session_anchors (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
message_id TEXT NOT NULL,
anchor_type TEXT NOT NULL,
content TEXT NOT NULL,
estimated_tokens INTEGER NOT NULL,
turn_index INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_session_anchors_project
ON session_anchors(project_id);"
).map_err(|e| format!("Migration (v3 tables) failed: {e}"))?;
Ok(())
@ -597,6 +610,91 @@ impl SessionDb {
Ok(metrics)
}
// --- Session anchors ---
pub fn save_session_anchors(&self, anchors: &[SessionAnchorRecord]) -> Result<(), String> {
if anchors.is_empty() {
return Ok(());
}
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"INSERT OR REPLACE INTO session_anchors (id, project_id, message_id, anchor_type, content, estimated_tokens, turn_index, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"
).map_err(|e| format!("Prepare anchor insert failed: {e}"))?;
for anchor in anchors {
stmt.execute(params![
anchor.id,
anchor.project_id,
anchor.message_id,
anchor.anchor_type,
anchor.content,
anchor.estimated_tokens,
anchor.turn_index,
anchor.created_at,
]).map_err(|e| format!("Insert anchor failed: {e}"))?;
}
Ok(())
}
pub fn load_session_anchors(&self, project_id: &str) -> Result<Vec<SessionAnchorRecord>, String> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT id, project_id, message_id, anchor_type, content, estimated_tokens, turn_index, created_at FROM session_anchors WHERE project_id = ?1 ORDER BY turn_index ASC"
).map_err(|e| format!("Query anchors failed: {e}"))?;
let anchors = stmt.query_map(params![project_id], |row| {
Ok(SessionAnchorRecord {
id: row.get(0)?,
project_id: row.get(1)?,
message_id: row.get(2)?,
anchor_type: row.get(3)?,
content: row.get(4)?,
estimated_tokens: row.get(5)?,
turn_index: row.get(6)?,
created_at: row.get(7)?,
})
}).map_err(|e| format!("Query anchors failed: {e}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Read anchor row failed: {e}"))?;
Ok(anchors)
}
pub fn delete_session_anchor(&self, id: &str) -> Result<(), String> {
let conn = self.conn.lock().unwrap();
conn.execute("DELETE FROM session_anchors WHERE id = ?1", params![id])
.map_err(|e| format!("Delete anchor failed: {e}"))?;
Ok(())
}
pub fn delete_project_anchors(&self, project_id: &str) -> Result<(), String> {
let conn = self.conn.lock().unwrap();
conn.execute("DELETE FROM session_anchors WHERE project_id = ?1", params![project_id])
.map_err(|e| format!("Delete project anchors failed: {e}"))?;
Ok(())
}
pub fn update_anchor_type(&self, id: &str, anchor_type: &str) -> Result<(), String> {
let conn = self.conn.lock().unwrap();
conn.execute(
"UPDATE session_anchors SET anchor_type = ?2 WHERE id = ?1",
params![id, anchor_type],
).map_err(|e| format!("Update anchor type failed: {e}"))?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionAnchorRecord {
pub id: String,
pub project_id: String,
pub message_id: String,
pub anchor_type: String,
pub content: String,
pub estimated_tokens: i64,
pub turn_index: i64,
pub created_at: i64,
}
#[cfg(test)]