use crate::context::ContextReader;
use crate::context::ir::{
build_tool_call, build_tool_result, extract_text, extract_tool_result_from_part,
};
use crate::message::{
Context, ContextListing, ConversationMessage, Entry, Part, ProviderId, ToolResultData,
};
use anyhow::Context as _;
use chrono::{DateTime, Utc};
use serde_json::Value;
use std::path::PathBuf;
pub struct CrushReader {
db_path: PathBuf,
}
impl CrushReader {
pub fn new(db_path: PathBuf) -> Self {
Self { db_path }
}
}
impl ContextReader for CrushReader {
fn list_contexts(&self) -> anyhow::Result<Vec<ContextListing>> {
let conn = crate::context::open_sqlite(&self.db_path)?;
let sql = "SELECT s.id, s.title, s.message_count, s.created_at, s.updated_at, s.parent_session_id \
FROM sessions s ORDER BY s.created_at DESC";
let mut stmt = conn.prepare(sql)?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, i64>(2)?,
row.get::<_, i64>(3)?,
row.get::<_, i64>(4)?,
row.get::<_, Option<String>>(5)?,
))
})?;
let mut contexts = Vec::new();
for row in rows {
let (id, title, count, created_at, updated_at, parent_id) = row?;
contexts.push(ContextListing {
id,
provider_id: ProviderId {
label: if title.is_empty() { None } else { Some(title) },
cwd: PathBuf::new(),
count: count.try_into().unwrap_or(0),
from: crush_timestamp(created_at),
until: crush_timestamp(updated_at),
},
path: self.db_path.clone(),
parent_id,
});
}
Ok(contexts)
}
fn delete_context(&self, context_id: &str) -> anyhow::Result<()> {
let mut conn = crate::context::open_sqlite(&self.db_path)?;
let tx = conn.transaction()?;
tx.execute(
"DELETE FROM messages WHERE session_id = ?1",
rusqlite::params![context_id],
)?;
tx.execute(
"DELETE FROM sessions WHERE id = ?1",
rusqlite::params![context_id],
)?;
tx.commit()?;
Ok(())
}
fn read_context(&self, context_id: &str) -> anyhow::Result<Context> {
let conn = crate::context::open_sqlite(&self.db_path)?;
let sql = "SELECT role, parts FROM messages WHERE session_id = ?1 ORDER BY created_at";
let mut stmt = conn.prepare(sql)?;
let rows = stmt.query_map(rusqlite::params![context_id], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
let mut messages = Vec::new();
let mut entries = Vec::new();
let mut previous_entry_id = String::new();
for (idx, row) in rows.enumerate() {
let (role, parts_str) = row?;
let parts: Value = serde_json::from_str(&parts_str)?;
let crush_msg = crush_build_message(&role, &parts)?;
let entry_id = format!("crush-{idx}");
if !crush_msg.parts.is_empty() || crush_msg.tool_results.is_empty() {
let parent_id = previous_entry_id.clone();
entries.push(Entry {
id: entry_id.clone(),
parent_id,
});
previous_entry_id.clone_from(&entry_id);
messages.push(ConversationMessage::new(
entry_id,
crush_msg.role,
crush_msg.parts,
));
}
for (tri, tr) in crush_msg.tool_results.into_iter().enumerate() {
let tr_entry_id = format!("crush-{idx}-tr{tri}");
let tr_parent = previous_entry_id.clone();
entries.push(Entry {
id: tr_entry_id.clone(),
parent_id: tr_parent,
});
previous_entry_id.clone_from(&tr_entry_id);
messages.push(ConversationMessage::new(
tr_entry_id,
"user",
vec![Part::ToolResult(tr)],
));
}
}
Ok(Context {
entries,
messages,
cwd: None,
})
}
}
struct CrushMessage {
role: String,
parts: Vec<Part>,
tool_results: Vec<ToolResultData>,
}
fn part_payload(part: &Value) -> &Value {
if part["data"].is_object() {
&part["data"]
} else {
part
}
}
fn crush_timestamp(ts: i64) -> Option<DateTime<Utc>> {
if ts > 100_000_000_000 {
DateTime::<Utc>::from_timestamp_millis(ts)
} else {
DateTime::<Utc>::from_timestamp(ts, 0)
}
}
fn crush_build_message(role: &str, parts: &Value) -> anyhow::Result<CrushMessage> {
let parts = parts.as_array().context("parts is not an array")?;
if role != "assistant" && role != "tool" {
let payloads = Value::Array(parts.iter().map(|p| part_payload(p).clone()).collect());
return Ok(CrushMessage {
role: role.to_string(),
parts: vec![Part::Text(extract_text(Some(&payloads)))],
tool_results: Vec::new(),
});
}
let mut out_parts = Vec::new();
let mut tool_results = Vec::new();
for part in parts {
let payload = part_payload(part);
match part["type"].as_str() {
Some("text") => {
if let Some(text) = payload["text"].as_str() {
out_parts.push(Part::Text(text.to_string()));
}
}
Some("reasoning") => {
if let Some(thinking) = payload["thinking"].as_str() {
out_parts.push(Part::Reasoning(crate::message::Reasoning::new(thinking)));
}
}
Some("tool_call" | "tool_use") => {
let id = payload["id"].as_str().unwrap_or("");
let name = payload["name"].as_str().unwrap_or("");
let parsed_input = payload["input"]
.as_str()
.and_then(|raw| serde_json::from_str::<Value>(raw).ok());
let arguments = parsed_input.as_ref().or_else(|| payload.get("input"));
out_parts.push(Part::ToolCall(build_tool_call(id, name, arguments)));
if let Some(res) = payload["result"].as_str() {
tool_results.push(build_tool_result(
id,
name.to_string(),
res.to_string(),
false,
));
}
}
Some("tool_result") => {
if let Some(id) = payload["tool_call_id"].as_str() {
tool_results.push(build_tool_result(
id,
payload["name"].as_str().unwrap_or("").to_string(),
payload["content"].as_str().unwrap_or("").to_string(),
payload["is_error"].as_bool().unwrap_or(false),
));
} else if let Some(tr) = extract_tool_result_from_part(part) {
tool_results.push(tr);
}
}
Some("shell_command") => {
out_parts.push(Part::Bash(crate::message::BashOutput {
command: payload["command"].as_str().unwrap_or("").to_string(),
output: payload["output"].as_str().unwrap_or("").to_string(),
}));
}
_ => {}
}
}
Ok(CrushMessage {
role: if role == "tool" {
"user".to_string()
} else {
"assistant".to_string()
},
parts: out_parts,
tool_results,
})
}