Skip to content

Commit 7f1e646

Browse files
committed
Implement JSONL memory store
1 parent 0e37094 commit 7f1e646

2 files changed

Lines changed: 193 additions & 23 deletions

File tree

codex-rs/memory/src/factory.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ pub fn open_repo_store(
3535
let be = backend.unwrap_or_else(choose_backend_from_env);
3636
Ok(match be {
3737
Backend::Jsonl => {
38-
let _path = std::env::var("CODEX_MEMORY_REPO_JSONL")
38+
let path = std::env::var("CODEX_MEMORY_REPO_JSONL")
3939
.map(std::path::PathBuf::from)
4040
.unwrap_or_else(|_| base.join("memory.jsonl"));
41-
Box::new(JsonlMemoryStore)
41+
Box::new(JsonlMemoryStore::new(path))
4242
}
4343
#[cfg(feature = "sqlite")]
4444
Backend::Sqlite => {
@@ -62,10 +62,10 @@ pub fn open_global_store(
6262
let be = backend.unwrap_or_else(choose_backend_from_env);
6363
Ok(match be {
6464
Backend::Jsonl => {
65-
let _path = std::env::var("CODEX_MEMORY_HOME_JSONL")
65+
let path = std::env::var("CODEX_MEMORY_HOME_JSONL")
6666
.map(std::path::PathBuf::from)
6767
.unwrap_or_else(|_| base.join("memory.jsonl"));
68-
Box::new(JsonlMemoryStore)
68+
Box::new(JsonlMemoryStore::new(path))
6969
}
7070
#[cfg(feature = "sqlite")]
7171
Backend::Sqlite => {
@@ -76,3 +76,28 @@ pub fn open_global_store(
7676
}
7777
})
7878
}
79+
80+
/// Rewrite a JSONL file, stripping invalid or empty lines.
81+
pub fn compact(path: &std::path::Path) -> anyhow::Result<()> {
82+
let data = match std::fs::read_to_string(path) {
83+
Ok(s) => s,
84+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
85+
Err(e) => return Err(e.into()),
86+
};
87+
let mut out = String::new();
88+
for line in data.lines() {
89+
let line = line.trim();
90+
if line.is_empty() {
91+
continue;
92+
}
93+
if let Ok(v) = serde_json::from_str::<serde_json::Value>(line) {
94+
out.push_str(&serde_json::to_string(&v)?);
95+
out.push('\n');
96+
}
97+
}
98+
if let Some(dir) = path.parent() {
99+
std::fs::create_dir_all(dir)?;
100+
}
101+
std::fs::write(path, out)?;
102+
Ok(())
103+
}

codex-rs/memory/src/store/jsonl.rs

Lines changed: 164 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,182 @@
11
use super::*;
22

3-
pub struct JsonlMemoryStore;
3+
#[derive(Debug, Clone)]
4+
pub struct JsonlMemoryStore {
5+
path: std::path::PathBuf,
6+
}
7+
8+
impl JsonlMemoryStore {
9+
pub fn new<P: Into<std::path::PathBuf>>(path: P) -> Self {
10+
Self { path: path.into() }
11+
}
12+
13+
fn read_all(&self) -> anyhow::Result<Vec<MemoryItem>> {
14+
let data = match std::fs::read_to_string(&self.path) {
15+
Ok(s) => s,
16+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
17+
Err(e) => return Err(e.into()),
18+
};
19+
let mut items = Vec::new();
20+
for line in data.lines() {
21+
let line = line.trim();
22+
if line.is_empty() {
23+
continue;
24+
}
25+
match serde_json::from_str::<MemoryItem>(line) {
26+
Ok(item) => items.push(item),
27+
Err(_) => continue, // skip noisy lines
28+
}
29+
}
30+
Ok(items)
31+
}
32+
33+
fn write_all(&self, items: &[MemoryItem]) -> anyhow::Result<()> {
34+
if let Some(dir) = self.path.parent() {
35+
std::fs::create_dir_all(dir)?;
36+
}
37+
let mut out = String::new();
38+
for it in items {
39+
out.push_str(&serde_json::to_string(it)?);
40+
out.push('\n');
41+
}
42+
std::fs::write(&self.path, out)?;
43+
Ok(())
44+
}
45+
}
446

547
impl MemoryStore for JsonlMemoryStore {
6-
fn add(&self, _item: MemoryItem) -> anyhow::Result<()> {
7-
todo!()
48+
fn add(&self, item: MemoryItem) -> anyhow::Result<()> {
49+
if let Some(dir) = self.path.parent() {
50+
std::fs::create_dir_all(dir)?;
51+
}
52+
let mut line = serde_json::to_string(&item)?;
53+
line.push('\n');
54+
use std::io::Write as _;
55+
let mut f = std::fs::OpenOptions::new()
56+
.create(true)
57+
.append(true)
58+
.open(&self.path)?;
59+
f.write_all(line.as_bytes())?;
60+
f.flush()?;
61+
Ok(())
862
}
9-
fn update(&self, _item: &MemoryItem) -> anyhow::Result<()> {
10-
todo!()
63+
64+
fn update(&self, item: &MemoryItem) -> anyhow::Result<()> {
65+
let mut items = self.read_all()?;
66+
for it in &mut items {
67+
if it.id == item.id {
68+
*it = item.clone();
69+
}
70+
}
71+
self.write_all(&items)
1172
}
12-
fn delete(&self, _id: &str) -> anyhow::Result<()> {
13-
todo!()
73+
74+
fn delete(&self, id: &str) -> anyhow::Result<()> {
75+
let items = self.read_all()?;
76+
let items: Vec<_> = items.into_iter().filter(|i| i.id != id).collect();
77+
self.write_all(&items)
1478
}
15-
fn get(&self, _id: &str) -> anyhow::Result<Option<MemoryItem>> {
16-
todo!()
79+
80+
fn get(&self, id: &str) -> anyhow::Result<Option<MemoryItem>> {
81+
let items = self.read_all()?;
82+
Ok(items.into_iter().find(|i| i.id == id))
1783
}
84+
1885
fn list(
1986
&self,
20-
_scope: Option<Scope>,
21-
_status: Option<Status>,
87+
scope: Option<Scope>,
88+
status: Option<Status>,
2289
) -> anyhow::Result<Vec<MemoryItem>> {
23-
todo!()
90+
let mut items = self.read_all()?;
91+
if let Some(sc) = scope {
92+
items.retain(|i| i.scope == sc);
93+
}
94+
if let Some(st) = status {
95+
items.retain(|i| i.status == st);
96+
}
97+
items.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
98+
Ok(items)
2499
}
25-
fn archive(&self, _id: &str, _archived: bool) -> anyhow::Result<()> {
26-
todo!()
100+
101+
fn archive(&self, id: &str, archived: bool) -> anyhow::Result<()> {
102+
let mut items = self.read_all()?;
103+
let st = if archived {
104+
Status::Archived
105+
} else {
106+
Status::Active
107+
};
108+
let mut found = false;
109+
for it in &mut items {
110+
if it.id == id {
111+
it.status = st.clone();
112+
found = true;
113+
}
114+
}
115+
if !found {
116+
anyhow::bail!("archive: id not found: {id}");
117+
}
118+
self.write_all(&items)
27119
}
28-
fn export(&self, _out: &mut dyn std::io::Write) -> anyhow::Result<()> {
29-
todo!()
120+
121+
fn export(&self, out: &mut dyn std::io::Write) -> anyhow::Result<()> {
122+
let items = self.list(None, None)?;
123+
for it in items {
124+
let line = serde_json::to_string(&it)?;
125+
out.write_all(line.as_bytes())?;
126+
out.write_all(b"\n")?;
127+
}
128+
Ok(())
30129
}
31-
fn import(&self, _input: &mut dyn std::io::Read) -> anyhow::Result<usize> {
32-
todo!()
130+
131+
fn import(&self, input: &mut dyn std::io::Read) -> anyhow::Result<usize> {
132+
let mut data = String::new();
133+
std::io::Read::read_to_string(input, &mut data)?;
134+
let items = self.read_all()?;
135+
let mut map: std::collections::HashMap<String, MemoryItem> =
136+
items.into_iter().map(|i| (i.id.clone(), i)).collect();
137+
let mut count = 0usize;
138+
for line in data.lines() {
139+
let line = line.trim();
140+
if line.is_empty() {
141+
continue;
142+
}
143+
match serde_json::from_str::<MemoryItem>(line) {
144+
Ok(item) => {
145+
map.insert(item.id.clone(), item);
146+
count += 1;
147+
}
148+
Err(_) => continue,
149+
}
150+
}
151+
let mut items: Vec<MemoryItem> = map.into_values().collect();
152+
items.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
153+
self.write_all(&items)?;
154+
Ok(count)
33155
}
156+
34157
fn stats(&self) -> anyhow::Result<serde_json::Value> {
35-
todo!()
158+
let items = self.read_all()?;
159+
let total = items.len();
160+
let active = items.iter().filter(|i| i.status == Status::Active).count();
161+
let archived = items
162+
.iter()
163+
.filter(|i| i.status == Status::Archived)
164+
.count();
165+
let mut by_scope = serde_json::Map::new();
166+
for sc in [Scope::Global, Scope::Repo, Scope::Dir] {
167+
let n = items.iter().filter(|i| i.scope == sc).count();
168+
let key = match sc {
169+
Scope::Global => "global",
170+
Scope::Repo => "repo",
171+
Scope::Dir => "dir",
172+
};
173+
by_scope.insert(key.to_string(), serde_json::json!(n));
174+
}
175+
Ok(serde_json::json!({
176+
"total": total,
177+
"active": active,
178+
"archived": archived,
179+
"by_scope": serde_json::Value::Object(by_scope),
180+
}))
36181
}
37182
}

0 commit comments

Comments
 (0)