Skip to main content

vtcode_session_store/
query.rs

1//! Read-only queries across sessions for analytics and long-term learning.
2
3use std::path::Path;
4
5use crate::error::SessionStoreError;
6use crate::sessions_root;
7
8/// Lightweight summary of a single session, read from its `manifest.json`.
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
10pub struct SessionSummary {
11    /// Session identifier (directory name).
12    pub session_id: String,
13    /// Number of completed turns.
14    pub turn_count: u64,
15    /// Total events recorded.
16    pub event_count: u64,
17    /// Lifecycle status.
18    pub status: String,
19    /// RFC3339 last-update timestamp (used for ordering).
20    pub updated_at: String,
21}
22
23/// A single grounded fact drawn from a session's memory envelope.
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
25pub struct FactRecord {
26    /// The fact text.
27    pub fact: String,
28    /// Session the fact originated from.
29    pub session_id: String,
30}
31
32/// List up to `n` most-recently-updated sessions.
33#[must_use]
34pub fn recent_sessions(workspace: &Path, n: usize) -> Vec<SessionSummary> {
35    let root = sessions_root(workspace);
36    if !root.exists() {
37        return Vec::new();
38    }
39    let mut out = Vec::new();
40    let entries = match std::fs::read_dir(&root) {
41        Ok(e) => e,
42        Err(_) => return Vec::new(),
43    };
44    for entry in entries.filter_map(Result::ok) {
45        let manifest = entry.path().join("manifest.json");
46        if let Ok(bytes) = std::fs::read(&manifest) {
47            if let Ok(s) = serde_json::from_slice::<SessionSummary>(&bytes) {
48                out.push(s);
49            }
50        }
51    }
52    out.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
53    out.truncate(n);
54    out
55}
56
57/// Cross-session long-term-learning query: collect grounded facts from every
58/// session's derived memory envelope. This is how the agent learns across
59/// sessions without loading any history into context.
60pub fn query_facts(workspace: &Path, limit: usize) -> Result<Vec<FactRecord>, SessionStoreError> {
61    let root = sessions_root(workspace);
62    if !root.exists() {
63        return Ok(Vec::new());
64    }
65    let mut facts: Vec<FactRecord> = Vec::new();
66    let entries = std::fs::read_dir(&root).map_err(|e| SessionStoreError::io(root.clone(), e))?;
67    for entry in entries.filter_map(Result::ok) {
68        let memory = entry.path().join(crate::DERIVED_DIR).join("memory.json");
69        let Ok(bytes) = std::fs::read(&memory) else {
70            continue;
71        };
72        let Ok(value) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
73            continue;
74        };
75        let session_id = entry.file_name().to_string_lossy().to_string();
76        if let Some(arr) = value.get("grounded_facts").and_then(|v| v.as_array()) {
77            for item in arr {
78                if let Some(fact) = item.get("fact").and_then(|f| f.as_str()) {
79                    facts.push(FactRecord {
80                        fact: fact.to_string(),
81                        session_id: session_id.clone(),
82                    });
83                }
84            }
85        }
86    }
87    facts.truncate(limit);
88    Ok(facts)
89}