mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
3936ed221d
## Why Codex memory updates currently rely on instructions that tell agents to create ad-hoc note files directly in the memory workspace. The memories extension already has a `MemoriesBackend` abstraction for local storage and future non-filesystem backends, so the ad-hoc note writer should live behind that same interface instead of baking local filesystem assumptions into the tool shape. ## What - Adds a `memories/add_ad_hoc_note` tool to the existing memories tool bundle. - Extends `MemoriesBackend` with `add_ad_hoc_note` plus request/response types so remote memory stores can implement the same operation later. - Implements the local backend by creating append-only notes under `extensions/ad_hoc/notes`. - Validates the tool-provided filename contract (`YYYY-MM-DDTHH-MM-SS-<slug>.md`), rejects path-like filenames, rejects empty notes, and uses create-new semantics so existing notes are never overwritten. - Keeps memories tool contribution behind the existing commented-out registration path; this defines the tool surface without newly exposing it through app-server. ## Test Plan - `just test -p codex-memories-extension`
130 lines
3.9 KiB
Rust
130 lines
3.9 KiB
Rust
use std::path::Component;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
|
|
use crate::backend::AddAdHocMemoryNoteRequest;
|
|
use crate::backend::AddAdHocMemoryNoteResponse;
|
|
use crate::backend::ListMemoriesRequest;
|
|
use crate::backend::ListMemoriesResponse;
|
|
use crate::backend::MemoriesBackend;
|
|
use crate::backend::MemoriesBackendError;
|
|
use crate::backend::ReadMemoryRequest;
|
|
use crate::backend::ReadMemoryResponse;
|
|
use crate::backend::SearchMemoriesRequest;
|
|
use crate::backend::SearchMemoriesResponse;
|
|
|
|
mod ad_hoc_note;
|
|
mod list;
|
|
mod path;
|
|
mod read;
|
|
mod search;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct LocalMemoriesBackend {
|
|
root: PathBuf,
|
|
}
|
|
|
|
impl LocalMemoriesBackend {
|
|
pub(crate) fn from_codex_home(codex_home: &AbsolutePathBuf) -> Self {
|
|
Self::from_memory_root(codex_home.join("memories").to_path_buf())
|
|
}
|
|
|
|
pub(crate) fn from_memory_root(root: impl Into<PathBuf>) -> Self {
|
|
Self { root: root.into() }
|
|
}
|
|
|
|
async fn resolve_scoped_path(
|
|
&self,
|
|
relative_path: Option<&str>,
|
|
) -> Result<PathBuf, MemoriesBackendError> {
|
|
let Some(relative_path) = relative_path else {
|
|
return Ok(self.root.clone());
|
|
};
|
|
let relative = Path::new(relative_path);
|
|
if relative.components().any(|component| {
|
|
matches!(
|
|
component,
|
|
Component::ParentDir | Component::RootDir | Component::Prefix(_)
|
|
)
|
|
}) {
|
|
return Err(MemoriesBackendError::invalid_path(
|
|
relative_path,
|
|
"must stay within the memories root",
|
|
));
|
|
}
|
|
if relative.components().any(path::is_hidden_component) {
|
|
return Err(MemoriesBackendError::NotFound {
|
|
path: relative_path.to_string(),
|
|
});
|
|
}
|
|
|
|
let components = relative.components().collect::<Vec<_>>();
|
|
let mut scoped_path = self.root.clone();
|
|
for (idx, component) in components.iter().enumerate() {
|
|
scoped_path.push(component.as_os_str());
|
|
|
|
let Some(metadata) = Self::metadata_or_none(&scoped_path).await? else {
|
|
for remaining_component in components.iter().skip(idx + 1) {
|
|
scoped_path.push(remaining_component.as_os_str());
|
|
}
|
|
return Ok(scoped_path);
|
|
};
|
|
|
|
path::reject_symlink(
|
|
&path::display_relative_path(&self.root, &scoped_path),
|
|
&metadata,
|
|
)?;
|
|
if idx + 1 < components.len() && !metadata.is_dir() {
|
|
return Err(MemoriesBackendError::invalid_path(
|
|
relative_path,
|
|
"traverses through a non-directory path component",
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(scoped_path)
|
|
}
|
|
|
|
async fn metadata_or_none(
|
|
path: &Path,
|
|
) -> Result<Option<std::fs::Metadata>, MemoriesBackendError> {
|
|
match tokio::fs::symlink_metadata(path).await {
|
|
Ok(metadata) => Ok(Some(metadata)),
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
|
Err(err) => Err(err.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MemoriesBackend for LocalMemoriesBackend {
|
|
async fn add_ad_hoc_note(
|
|
&self,
|
|
request: AddAdHocMemoryNoteRequest,
|
|
) -> Result<AddAdHocMemoryNoteResponse, MemoriesBackendError> {
|
|
ad_hoc_note::add_ad_hoc_note(self, request).await
|
|
}
|
|
|
|
async fn list(
|
|
&self,
|
|
request: ListMemoriesRequest,
|
|
) -> Result<ListMemoriesResponse, MemoriesBackendError> {
|
|
list::list(self, request).await
|
|
}
|
|
|
|
async fn read(
|
|
&self,
|
|
request: ReadMemoryRequest,
|
|
) -> Result<ReadMemoryResponse, MemoriesBackendError> {
|
|
read::read(self, request).await
|
|
}
|
|
|
|
async fn search(
|
|
&self,
|
|
request: SearchMemoriesRequest,
|
|
) -> Result<SearchMemoriesResponse, MemoriesBackendError> {
|
|
search::search(self, request).await
|
|
}
|
|
}
|