1use super::*;
2use rusqlite::{Connection, OpenFlags};
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
5pub struct DatabaseSession {
6 pub id: String,
7 pub title: Option<String>,
8 pub cwd: Option<String>,
9}
10fn open(agent: Agent, path: &Path, opts: &ReadOptions) -> Result<Connection, ImportError> {
11 if !matches!(
12 agent,
13 Agent::OpenCode
14 | Agent::Cursor
15 | Agent::Goose
16 | Agent::Hermes
17 | Agent::ZCode
18 | Agent::CursorCli
19 | Agent::Zed
20 | Agent::Warp
21 ) {
22 return Err(ReadError::InvalidOptions("no database adapter for this agent").into());
23 }
24 if opts.stop_at_byte.is_some() {
25 return Err(ReadError::InvalidOptions(
26 "database snapshots do not have byte-prefix boundaries",
27 )
28 .into());
29 }
30 let size = std::fs::metadata(path).map_err(ReadError::Io)?.len();
31 if let Some(limit) = opts.max_file_bytes
32 && size > limit
33 {
34 return Err(ReadError::TooLarge { limit }.into());
35 }
36 Connection::open_with_flags(
37 path,
38 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
39 )
40 .map_err(|e| ReadError::Database(e).into())
41}
42pub fn list_database_sessions(
44 agent: Agent,
45 path: &Path,
46 opts: &ReadOptions,
47) -> Result<Vec<DatabaseSession>, ImportError> {
48 let conn = open(agent, path, opts)?;
49 let mut result = Vec::new();
50 if matches!(agent, Agent::OpenCode | Agent::ZCode) {
51 let mut statement = conn
52 .prepare("SELECT id,title,directory FROM session ORDER BY time_created,id")
53 .map_err(ReadError::Database)?;
54 let rows = statement
55 .query_map([], |r| {
56 Ok(DatabaseSession {
57 id: r.get(0)?,
58 title: r.get(1)?,
59 cwd: r.get(2)?,
60 })
61 })
62 .map_err(ReadError::Database)?;
63 for row in rows {
64 result.push(row.map_err(ReadError::Database)?);
65 }
66 } else if agent == Agent::Goose {
67 let mut statement = conn
68 .prepare("SELECT id,name,working_dir FROM sessions ORDER BY created_at,id")
69 .map_err(ReadError::Database)?;
70 let rows = statement
71 .query_map([], |r| {
72 Ok(DatabaseSession {
73 id: r.get(0)?,
74 title: r.get(1)?,
75 cwd: r.get(2)?,
76 })
77 })
78 .map_err(ReadError::Database)?;
79 for row in rows {
80 result.push(row.map_err(ReadError::Database)?);
81 }
82 } else if agent == Agent::Hermes {
83 let mut statement = conn
84 .prepare("SELECT id,title,cwd FROM sessions ORDER BY started_at,id")
85 .map_err(ReadError::Database)?;
86 for row in statement
87 .query_map([], |r| {
88 Ok(DatabaseSession {
89 id: r.get(0)?,
90 title: r.get(1)?,
91 cwd: r.get(2)?,
92 })
93 })
94 .map_err(ReadError::Database)?
95 {
96 result.push(row.map_err(ReadError::Database)?);
97 }
98 } else if agent == Agent::Warp {
99 let mut statement=conn.prepare("SELECT conversation_id,summary FROM agent_conversations ORDER BY last_modified_at,id").map_err(ReadError::Database)?;
100 for row in statement
101 .query_map([], |r| {
102 Ok(DatabaseSession {
103 id: r.get(0)?,
104 title: r.get(1)?,
105 cwd: None,
106 })
107 })
108 .map_err(ReadError::Database)?
109 {
110 result.push(row.map_err(ReadError::Database)?);
111 }
112 } else if agent == Agent::CursorCli {
113 let meta = super::cursor_cli::metadata(&conn, opts)?;
114 result.push(DatabaseSession {
115 id: crate::parser::required(&meta, "agentId")?.into(),
116 title: crate::parser::string(&meta, "name"),
117 cwd: None,
118 });
119 } else if agent == Agent::Zed {
120 let mut statement = conn
121 .prepare("SELECT id,summary FROM threads ORDER BY updated_at,id")
122 .map_err(ReadError::Database)?;
123 for row in statement
124 .query_map([], |r| {
125 Ok(DatabaseSession {
126 id: r.get(0)?,
127 title: r.get(1)?,
128 cwd: None,
129 })
130 })
131 .map_err(ReadError::Database)?
132 {
133 result.push(row.map_err(ReadError::Database)?);
134 }
135 } else {
136 let mut statement = conn
137 .prepare("SELECT key FROM cursorDiskKV WHERE key LIKE 'composerData:%' ORDER BY key")
138 .map_err(ReadError::Database)?;
139 let rows = statement
140 .query_map([], |r| r.get::<_, String>(0))
141 .map_err(ReadError::Database)?;
142 for row in rows {
143 let key = row.map_err(ReadError::Database)?;
144 result.push(DatabaseSession {
145 id: key["composerData:".len()..].into(),
146 title: None,
147 cwd: None,
148 });
149 }
150 }
151 Ok(result)
152}
153fn json(conn: &Connection, sql: &str, key: &str, opts: &ReadOptions) -> Result<Value, ImportError> {
154 let mut stmt = conn.prepare(sql).map_err(ReadError::Database)?;
156 let mut rows = stmt.query([key]).map_err(ReadError::Database)?;
157 let row = rows
158 .next()
159 .map_err(ReadError::Database)?
160 .ok_or(LineErrorKind::MissingField("database record"))?;
161 let size = row.get::<_, u64>(0).map_err(ReadError::Database)?;
162 if opts.max_line_bytes.is_some_and(|limit| size > limit as u64) {
163 return Err(LineErrorKind::TooLong.into());
164 }
165 let bytes = row.get::<_, Vec<u8>>(1).map_err(ReadError::Database)?;
166 serde_json::from_slice(&bytes).map_err(|_| LineErrorKind::InvalidJson.into())
167}
168fn source(table: &str, key: &str) -> ImportSource {
169 ImportSource::DatabaseRow {
170 table: table.into(),
171 key: key.into(),
172 }
173}
174pub fn import_database(
177 agent: Agent,
178 path: &Path,
179 id: &str,
180 opts: &ReadOptions,
181) -> Result<SessionImport, ImportError> {
182 let mut conn = open(agent, path, opts)?;
183 let tx = conn.transaction().map_err(ReadError::Database)?;
184 let mut result = SessionImport::default();
185 let mut state = State {
186 session_id: Some(id.into()),
187 ..Default::default()
188 };
189 if agent == Agent::Warp {
190 super::warp::import(&tx, id, opts, &mut result)?;
191 } else if agent == Agent::CursorCli {
192 super::cursor_cli::import(&tx, id, opts, &mut result)?;
193 } else if agent == Agent::Zed {
194 let (data_type, parent, length): (String, Option<String>, u64) = tx
195 .query_row(
196 "SELECT data_type,parent_id,length(data) FROM threads WHERE id=?1",
197 [id],
198 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
199 )
200 .map_err(ReadError::Database)?;
201 if opts.max_line_bytes.is_some_and(|n| length > n as u64) {
202 return Err(LineErrorKind::TooLong.into());
203 }
204 let data: Vec<u8> = tx
205 .query_row("SELECT data FROM threads WHERE id=?1", [id], |r| r.get(0))
206 .map_err(ReadError::Database)?;
207 let bytes = match data_type.as_str() {
208 "json" => data,
209 "zstd" => {
210 let decoder =
211 zstd::stream::read::Decoder::new(data.as_slice()).map_err(ReadError::Io)?;
212 let limit = opts.max_line_bytes.map(|n| n as u64).unwrap_or(u64::MAX);
213 let mut decoded = Vec::new();
214 std::io::Read::read_to_end(
215 &mut std::io::Read::take(decoder, limit.saturating_add(1)),
216 &mut decoded,
217 )
218 .map_err(ReadError::Io)?;
219 if decoded.len() as u64 > limit {
220 return Err(LineErrorKind::TooLong.into());
221 }
222 decoded
223 }
224 _ => return Err(LineErrorKind::InvalidField("threads.data_type".into()).into()),
225 };
226 let value: Value =
227 serde_json::from_slice(&bytes).map_err(|_| LineErrorKind::InvalidJson)?;
228 super::document(agent, &value, opts, &mut result)?;
229 for event in &mut result.events {
230 event.sources = vec![source("threads", id)];
231 event.session_id = Some(id.into());
232 if let Event::Message(m) = &mut event.value {
233 m.is_sidechain |= parent.is_some();
234 }
235 }
236 } else if matches!(agent, Agent::OpenCode | Agent::ZCode) {
237 let (directory, parent): (String, Option<String>) = tx
238 .query_row(
239 "SELECT directory,parent_id FROM session WHERE id=?1",
240 [id],
241 |r| Ok((r.get(0)?, r.get(1)?)),
242 )
243 .map_err(ReadError::Database)?;
244 state.sidechain = parent.is_some();
245 emit_meta(
246 MetaUpdate {
247 session_id: Some(id.into()),
248 cwd: Some(directory),
249 ..Default::default()
250 },
251 vec![source("session", id)],
252 opts,
253 &mut result,
254 );
255 let ids = {
256 let mut stmt = tx
257 .prepare(if agent == Agent::ZCode {
258 "SELECT id FROM message WHERE session_id=?1 ORDER BY sequence,id"
259 } else {
260 "SELECT id FROM message WHERE session_id=?1 ORDER BY time_created,id"
261 })
262 .map_err(ReadError::Database)?;
263 stmt.query_map([id], |r| r.get::<_, String>(0))
264 .map_err(ReadError::Database)?
265 .collect::<Result<Vec<_>, _>>()
266 .map_err(ReadError::Database)?
267 };
268 for message_id in ids {
269 let mut info = json(
270 &tx,
271 "SELECT length(CAST(data AS BLOB)),CAST(data AS BLOB) FROM message WHERE id=?1",
272 &message_id,
273 opts,
274 )?;
275 if !info.is_object() {
276 return Err(LineErrorKind::InvalidField("message.data".into()).into());
277 }
278 info["id"] = Value::String(message_id.clone());
279 info["sessionID"] = Value::String(id.into());
280 let ids = {
281 let mut stmt=tx.prepare(if agent == Agent::ZCode { "SELECT id FROM part WHERE message_id=?1 AND session_id=?2 ORDER BY sequence,id" } else { "SELECT id FROM part WHERE message_id=?1 AND session_id=?2 ORDER BY time_created,id" }).map_err(ReadError::Database)?;
282 stmt.query_map([&message_id, id], |r| r.get::<_, String>(0))
283 .map_err(ReadError::Database)?
284 .collect::<Result<Vec<_>, _>>()
285 .map_err(ReadError::Database)?
286 };
287 let mut parts = Vec::new();
288 let mut sources = vec![source("message", &message_id)];
289 for part_id in ids {
290 parts.push(json(
291 &tx,
292 "SELECT length(CAST(data AS BLOB)),CAST(data AS BLOB) FROM part WHERE id=?1",
293 &part_id,
294 opts,
295 )?);
296 sources.push(source("part", &part_id));
297 }
298 let record = serde_json::json!({"info":info,"parts":parts});
299 parse_record(agent, &record, &mut state, sources, opts, &mut result)?;
300 result.summary.lines += 1;
301 }
302 } else if agent == Agent::Goose {
303 let directory: String = tx
304 .query_row("SELECT working_dir FROM sessions WHERE id=?1", [id], |r| {
305 r.get(0)
306 })
307 .map_err(ReadError::Database)?;
308 emit_meta(
309 MetaUpdate {
310 session_id: Some(id.into()),
311 cwd: Some(directory),
312 ..Default::default()
313 },
314 vec![source("sessions", id)],
315 opts,
316 &mut result,
317 );
318 let mut stmt=tx.prepare("SELECT id,message_id,role,created_timestamp,length(CAST(content_json AS BLOB)),CAST(content_json AS BLOB),length(CAST(metadata_json AS BLOB)),CAST(metadata_json AS BLOB) FROM messages WHERE session_id=?1 ORDER BY created_timestamp,id").map_err(ReadError::Database)?;
319 let mut rows = stmt.query([id]).map_err(ReadError::Database)?;
320 while let Some(row) = rows.next().map_err(ReadError::Database)? {
321 let content_size: u64 = row.get(4).map_err(ReadError::Database)?;
322 let metadata_size: Option<u64> = row.get(6).map_err(ReadError::Database)?;
323 if opts.max_line_bytes.is_some_and(|limit| {
324 content_size.saturating_add(metadata_size.unwrap_or(0)) > limit as u64
325 }) {
326 return Err(LineErrorKind::TooLong.into());
327 }
328 let content: Vec<u8> = row.get(5).map_err(ReadError::Database)?;
329 let metadata: Option<Vec<u8>> = row.get(7).map_err(ReadError::Database)?;
330 let content: Value =
331 serde_json::from_slice(&content).map_err(|_| LineErrorKind::InvalidJson)?;
332 let metadata: Value = metadata
333 .map(|v| serde_json::from_slice(&v))
334 .transpose()
335 .map_err(|_| LineErrorKind::InvalidJson)?
336 .unwrap_or(Value::Null);
337 let row_id: i64 = row.get(0).map_err(ReadError::Database)?;
338 let message_id: Option<String> = row.get(1).map_err(ReadError::Database)?;
339 let role: String = row.get(2).map_err(ReadError::Database)?;
340 let created: i64 = row.get(3).map_err(ReadError::Database)?;
341 let record = serde_json::json!({"id":message_id,"role":role,"created":created,"content":content,"metadata":metadata});
342 parse_record(
343 agent,
344 &record,
345 &mut state,
346 vec![source("messages", &row_id.to_string())],
347 opts,
348 &mut result,
349 )?;
350 result.summary.lines += 1;
351 }
352 } else if agent == Agent::Hermes {
353 let (model, cwd, parent): (Option<String>, Option<String>, Option<String>) = tx
354 .query_row(
355 "SELECT model,cwd,parent_session_id FROM sessions WHERE id=?1",
356 [id],
357 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
358 )
359 .map_err(ReadError::Database)?;
360 state.sidechain = parent.is_some();
361 emit_meta(
362 MetaUpdate {
363 session_id: Some(id.into()),
364 model,
365 cwd,
366 ..Default::default()
367 },
368 vec![source("sessions", id)],
369 opts,
370 &mut result,
371 );
372 let mut stmt = tx.prepare("SELECT id,role,length(CAST(content AS BLOB)),content,tool_call_id,length(CAST(tool_calls AS BLOB)),tool_calls,timestamp FROM messages WHERE session_id=?1 ORDER BY timestamp,id").map_err(ReadError::Database)?;
373 let mut rows = stmt.query([id]).map_err(ReadError::Database)?;
374 while let Some(row) = rows.next().map_err(ReadError::Database)? {
375 let content_size: Option<u64> = row.get(2).map_err(ReadError::Database)?;
376 let calls_size: Option<u64> = row.get(5).map_err(ReadError::Database)?;
377 if opts.max_line_bytes.is_some_and(|limit| {
378 content_size
379 .unwrap_or(0)
380 .saturating_add(calls_size.unwrap_or(0))
381 > limit as u64
382 }) {
383 return Err(LineErrorKind::TooLong.into());
384 }
385 let row_id: i64 = row.get(0).map_err(ReadError::Database)?;
386 let role: String = row.get(1).map_err(ReadError::Database)?;
387 let content: Option<String> = row.get(3).map_err(ReadError::Database)?;
388 let content = match content {
390 Some(s) if s.starts_with("\0json:") => {
391 serde_json::from_str(&s[6..]).map_err(|_| LineErrorKind::InvalidJson)?
392 }
393 Some(s) => Value::String(s),
394 None => Value::Null,
395 };
396 let call_id: Option<String> = row.get(4).map_err(ReadError::Database)?;
397 let calls: Option<String> = row.get(6).map_err(ReadError::Database)?;
398 let calls: Value = calls
399 .map(|s| serde_json::from_str(&s))
400 .transpose()
401 .map_err(|_| LineErrorKind::InvalidJson)?
402 .unwrap_or(Value::Null);
403 let timestamp: Option<f64> = row.get(7).map_err(ReadError::Database)?;
404 let mut record = serde_json::json!({"role":role,"content":content,"tool_call_id":call_id,"timestamp":timestamp});
405 if !calls.is_null() {
406 record["tool_calls"] = calls;
407 }
408 parse_record(
409 agent,
410 &record,
411 &mut state,
412 vec![source("messages", &row_id.to_string())],
413 opts,
414 &mut result,
415 )?;
416 result.summary.lines += 1;
417 }
418 tally(
419 &mut result.summary.ignored_types,
420 "hermes:session-totals-not-message-usage",
421 );
422 } else {
423 let key = format!("composerData:{id}");
424 let composer = json(
425 &tx,
426 "SELECT length(CAST(value AS BLOB)),CAST(value AS BLOB) FROM cursorDiskKV WHERE key=?1",
427 &key,
428 opts,
429 )?;
430 emit_meta(
431 MetaUpdate {
432 session_id: Some(id.into()),
433 model: composer
434 .pointer("/modelConfig/modelName")
435 .and_then(Value::as_str)
436 .map(str::to_owned),
437 ..Default::default()
438 },
439 vec![source("cursorDiskKV", &key)],
440 opts,
441 &mut result,
442 );
443 let headers = composer
444 .get("fullConversationHeadersOnly")
445 .and_then(Value::as_array)
446 .ok_or(LineErrorKind::MissingField("fullConversationHeadersOnly"))?;
447 for header in headers {
448 let bubble = crate::parser::required(header, "bubbleId")?;
449 let key = format!("bubbleId:{id}:{bubble}");
450 let record = json(
451 &tx,
452 "SELECT length(CAST(value AS BLOB)),CAST(value AS BLOB) FROM cursorDiskKV WHERE key=?1",
453 &key,
454 opts,
455 )?;
456 parse_record(
457 agent,
458 &record,
459 &mut state,
460 vec![source("cursorDiskKV", &key)],
461 opts,
462 &mut result,
463 )?;
464 result.summary.lines += 1;
465 }
466 }
467 result.summary.status = ReadStatus::Complete;
469 Ok(result)
470}