Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions codex-rs/memory/src/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ pub fn open_repo_store(
let be = backend.unwrap_or_else(choose_backend_from_env);
Ok(match be {
Backend::Jsonl => {
let _path = std::env::var("CODEX_MEMORY_REPO_JSONL")
let path = std::env::var("CODEX_MEMORY_REPO_JSONL")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| base.join("memory.jsonl"));
Box::new(JsonlMemoryStore)
Box::new(JsonlMemoryStore::new(path))
}
#[cfg(feature = "sqlite")]
Backend::Sqlite => {
Expand All @@ -62,10 +62,10 @@ pub fn open_global_store(
let be = backend.unwrap_or_else(choose_backend_from_env);
Ok(match be {
Backend::Jsonl => {
let _path = std::env::var("CODEX_MEMORY_HOME_JSONL")
let path = std::env::var("CODEX_MEMORY_HOME_JSONL")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| base.join("memory.jsonl"));
Box::new(JsonlMemoryStore)
Box::new(JsonlMemoryStore::new(path))
}
#[cfg(feature = "sqlite")]
Backend::Sqlite => {
Expand Down
29 changes: 26 additions & 3 deletions codex-rs/memory/src/recall.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::types::Kind;
use crate::types::MemoryItem;
use crate::types::Scope;
use crate::types::Status;

pub struct RecallContext {
pub repo_root: Option<std::path::PathBuf>,
Expand All @@ -13,9 +16,29 @@ pub struct RecallContext {
}

pub fn recall(
_store: &dyn crate::store::MemoryStore,
store: &dyn crate::store::MemoryStore,
_prompt: &str,
_ctx: &RecallContext,
ctx: &RecallContext,
) -> anyhow::Result<Vec<MemoryItem>> {
todo!()
// Basic implementation: list active repo-scoped memories and return up to
// `item_cap` items without exceeding `token_cap` characters.
let mut items = store.list(Some(Scope::Repo), Some(Status::Active))?;
// Prefer preferences and facts first; stable sort by updated_at desc.
items.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
items.retain(|it| matches!(it.kind, Kind::Pref | Kind::Fact));
if items.len() > ctx.item_cap {
items.truncate(ctx.item_cap);
}
// Rough token cap approximation: 1 token ~ 1 char.
let mut total = 0usize;
let mut out = Vec::new();
for it in items {
let len = it.content.len();
if !out.is_empty() && total + len > ctx.token_cap {
break;
}
total += len;
out.push(it);
}
Ok(out)
}
152 changes: 133 additions & 19 deletions codex-rs/memory/src/store/jsonl.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,151 @@
use super::*;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;

pub struct JsonlMemoryStore;
/// Simple JSONL-backed memory store used for both durable history and recall.
pub struct JsonlMemoryStore {
path: PathBuf,
}

impl JsonlMemoryStore {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Self {
path: path.as_ref().to_path_buf(),
}
}

fn read_all(&self) -> anyhow::Result<Vec<MemoryItem>> {
let data = match std::fs::read_to_string(&self.path) {
Ok(s) => s,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(e) => return Err(e.into()),
};
let mut items = Vec::new();
for line in data.lines() {
if line.trim().is_empty() {
continue;
}
if let Ok(it) = serde_json::from_str::<MemoryItem>(line) {
items.push(it);
}
}
Ok(items)
}

fn write_all(&self, items: &[MemoryItem]) -> anyhow::Result<()> {
let mut out = String::new();
for it in items {
let line = serde_json::to_string(it)?;
out.push_str(&line);
out.push('\n');
}
if let Some(dir) = self.path.parent() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(&self.path, out)?;
Ok(())
}
}

impl MemoryStore for JsonlMemoryStore {
fn add(&self, _item: MemoryItem) -> anyhow::Result<()> {
todo!()
fn add(&self, item: MemoryItem) -> anyhow::Result<()> {
if let Some(dir) = self.path.parent() {
std::fs::create_dir_all(dir)?;
}
let mut line = serde_json::to_string(&item)?;
line.push('\n');
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
f.write_all(line.as_bytes())?;
f.flush()?;
Ok(())
}
fn update(&self, _item: &MemoryItem) -> anyhow::Result<()> {
todo!()

fn update(&self, item: &MemoryItem) -> anyhow::Result<()> {
let mut items = self.read_all()?;
for it in &mut items {
if it.id == item.id {
*it = item.clone();
}
}
self.write_all(&items)
}
fn delete(&self, _id: &str) -> anyhow::Result<()> {
todo!()

fn delete(&self, id: &str) -> anyhow::Result<()> {
let items = self.read_all()?;
let items: Vec<_> = items.into_iter().filter(|i| i.id != id).collect();
self.write_all(&items)
}
fn get(&self, _id: &str) -> anyhow::Result<Option<MemoryItem>> {
todo!()

fn get(&self, id: &str) -> anyhow::Result<Option<MemoryItem>> {
let items = self.read_all()?;
Ok(items.into_iter().find(|i| i.id == id))
}

fn list(
&self,
_scope: Option<Scope>,
_status: Option<Status>,
scope: Option<Scope>,
status: Option<Status>,
) -> anyhow::Result<Vec<MemoryItem>> {
todo!()
let mut items = self.read_all()?;
if let Some(sc) = scope {
items.retain(|i| i.scope == sc);
}
if let Some(st) = status {
items.retain(|i| i.status == st);
}
Ok(items)
}
fn archive(&self, _id: &str, _archived: bool) -> anyhow::Result<()> {
todo!()

fn archive(&self, id: &str, archived: bool) -> anyhow::Result<()> {
let mut items = self.read_all()?;
for it in &mut items {
if it.id == id {
it.status = if archived {
Status::Archived
} else {
Status::Active
};
}
}
self.write_all(&items)
}
fn export(&self, _out: &mut dyn std::io::Write) -> anyhow::Result<()> {
todo!()

fn export(&self, out: &mut dyn std::io::Write) -> anyhow::Result<()> {
let items = self.read_all()?;
for it in items {
let line = serde_json::to_string(&it)?;
out.write_all(line.as_bytes())?;
out.write_all(b"\n")?;
}
Ok(())
}
fn import(&self, _input: &mut dyn std::io::Read) -> anyhow::Result<usize> {
todo!()

fn import(&self, input: &mut dyn std::io::Read) -> anyhow::Result<usize> {
let mut buf = String::new();
std::io::Read::read_to_string(input, &mut buf)?;
let mut items = self.read_all()?;
let mut count = 0usize;
for line in buf.lines() {
if line.trim().is_empty() {
continue;
}
if let Ok(it) = serde_json::from_str::<MemoryItem>(line) {
items.push(it);
count += 1;
}
}
self.write_all(&items)?;
Ok(count)
}

fn stats(&self) -> anyhow::Result<serde_json::Value> {
todo!()
let items = self.read_all()?;
let total = items.len();
let active = items.iter().filter(|i| i.status == Status::Active).count();
Ok(serde_json::json!({"total": total, "active": active}))
}
}
4 changes: 2 additions & 2 deletions codex-rs/tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ vt100-tests = []
# Gate verbose debug logging inside the TUI implementation.
debug-logs = []
# Opt-in: allow using SQLite memory backend from codex-memory.
memory-sqlite = ["codex-memory/sqlite", "codex-memory"]
memory-sqlite = ["codex-memory/sqlite"]

[lints]
workspace = true
Expand All @@ -39,7 +39,7 @@ codex-common = { path = "../common", features = [
codex-core = { path = "../core" }
codex-file-search = { path = "../file-search" }
codex-login = { path = "../login" }
codex-memory = { path = "../memory", optional = true }
codex-memory = { path = "../memory" }
codex-ollama = { path = "../ollama" }
codex-protocol = { path = "../protocol" }
color-eyre = "0.6.3"
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ pub mod insert_history;
pub mod live_wrap;
mod markdown;
mod markdown_stream;
mod memory;
pub mod memories_panel;
pub mod memory;
pub mod onboarding;
mod pager_overlay;
mod render;
Expand Down
90 changes: 90 additions & 0 deletions codex-rs/tui/src/memories_panel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use chrono::Utc;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::widgets::{Widget, WidgetRef};
use uuid::Uuid;

use codex_memory::factory;
use codex_memory::types::Counters;
use codex_memory::types::Kind;
use codex_memory::types::MemoryItem;
use codex_memory::types::RelevanceHints;
use codex_memory::types::Scope;
use codex_memory::types::Status;

/// Simple panel showing stored memories and exposing minimal CRUD ops.
pub struct MemoriesPanel {
repo_root: std::path::PathBuf,
items: Vec<MemoryItem>,
}

impl MemoriesPanel {
pub fn new(repo_root: std::path::PathBuf) -> anyhow::Result<Self> {
let mut panel = Self {
repo_root,
items: Vec::new(),
};
panel.refresh()?;
Ok(panel)
}

/// Reload items from the repo store.
pub fn refresh(&mut self) -> anyhow::Result<()> {
let store = factory::open_repo_store(&self.repo_root, None)?;
self.items = store.list(Some(Scope::Repo), Some(Status::Active))?;
Ok(())
}

/// Add a new preference memory entry.
pub fn add_pref(&mut self, text: &str) -> anyhow::Result<()> {
let ts = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let item = MemoryItem {
id: Uuid::new_v4().to_string(),
created_at: ts.clone(),
updated_at: ts,
schema_version: 1,
source: "codex-tui".to_string(),
scope: Scope::Repo,
status: Status::Active,
kind: Kind::Pref,
content: text.to_string(),
tags: vec!["pref".to_string()],
relevance_hints: RelevanceHints {
files: vec![],
crates: vec![],
languages: vec![],
commands: vec![],
},
counters: Counters {
seen_count: 0,
used_count: 0,
last_used_at: None,
},
expiry: None,
};
let store = factory::open_repo_store(&self.repo_root, None)?;
store.add(item)?;
self.refresh()
}

/// Delete a memory item by id.
pub fn delete(&mut self, id: &str) -> anyhow::Result<()> {
let store = factory::open_repo_store(&self.repo_root, None)?;
store.delete(id)?;
self.refresh()
}
}

impl WidgetRef for &MemoriesPanel {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let mut lines: Vec<Line<'static>> = Vec::with_capacity(self.items.len() + 1);
lines.push(Line::raw("Memories:"));
for it in &self.items {
lines.push(Line::raw(format!("- {}", it.content)));
}
let para = Paragraph::new(lines);
para.render(area, buf);
}
}
Loading
Loading