skilllite_executor/
session.rs1use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fs;
9use std::path::Path;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SessionEntry {
13 pub session_id: String,
14 pub session_key: String,
15 pub updated_at: String,
16 #[serde(default)]
17 pub input_tokens: u64,
18 #[serde(default)]
19 pub output_tokens: u64,
20 #[serde(default)]
21 pub total_tokens: u64,
22 #[serde(default)]
23 pub context_tokens: u64,
24 #[serde(default)]
25 pub compaction_count: u32,
26 #[serde(default)]
27 pub memory_flush_at: Option<String>,
28 #[serde(default)]
29 pub memory_flush_compaction_count: Option<u32>,
30 #[serde(flatten)]
31 pub extra: HashMap<String, serde_json::Value>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, Default)]
35pub struct SessionStore {
36 pub sessions: HashMap<String, SessionEntry>,
37}
38
39impl SessionStore {
40 pub fn load(path: &Path) -> Result<Self> {
41 if !path.exists() {
42 return Ok(Self::default());
43 }
44 let content = fs::read_to_string(path)
45 .with_context(|| format!("Failed to read session store: {}", path.display()))?;
46 serde_json::from_str(&content).with_context(|| "Invalid sessions.json")
47 }
48
49 pub fn save(&self, path: &Path) -> Result<()> {
50 if let Some(parent) = path.parent() {
51 fs::create_dir_all(parent)?;
52 }
53 let content = serde_json::to_string_pretty(self)?;
54 fs::write(path, content)
55 .with_context(|| format!("Failed to write session store: {}", path.display()))
56 }
57
58 pub fn get(&self, session_key: &str) -> Option<&SessionEntry> {
59 self.sessions.get(session_key)
60 }
61
62 pub fn create_or_get(&mut self, session_key: &str) -> &mut SessionEntry {
63 let now = chrono_now();
64 let entry = self
65 .sessions
66 .entry(session_key.to_string())
67 .or_insert_with(|| SessionEntry {
68 session_id: format!("tx-{}", uuid_short()),
69 session_key: session_key.to_string(),
70 updated_at: now.clone(),
71 input_tokens: 0,
72 output_tokens: 0,
73 total_tokens: 0,
74 context_tokens: 0,
75 compaction_count: 0,
76 memory_flush_at: None,
77 memory_flush_compaction_count: None,
78 extra: HashMap::new(),
79 });
80 entry.updated_at = now;
81 entry
82 }
83
84 pub fn update(&mut self, session_key: &str, f: impl FnOnce(&mut SessionEntry)) -> Result<()> {
85 let entry = self
86 .sessions
87 .get_mut(session_key)
88 .context("Session not found")?;
89 f(entry);
90 entry.updated_at = chrono_now();
91 Ok(())
92 }
93
94 pub fn reset_compaction_state(&mut self, session_key: &str) {
96 if let Some(entry) = self.sessions.get_mut(session_key) {
97 entry.compaction_count = 0;
98 entry.memory_flush_at = None;
99 entry.memory_flush_compaction_count = None;
100 entry.updated_at = chrono_now();
101 }
102 }
103}
104
105fn chrono_now() -> String {
106 use std::time::{SystemTime, UNIX_EPOCH};
107 let secs = SystemTime::now()
108 .duration_since(UNIX_EPOCH)
109 .map(|d| d.as_secs())
110 .unwrap_or(0);
111 format!("{}", secs)
112}
113
114fn uuid_short() -> String {
115 use std::time::{SystemTime, UNIX_EPOCH};
116 let t = SystemTime::now()
117 .duration_since(UNIX_EPOCH)
118 .map(|d| d.as_nanos())
119 .unwrap_or(0);
120 format!("{:x}", t % 0xFFFF_FFFF)
121}