From ff27d01676a93be7467b3893e82f41a7af7e1418 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Fri, 1 May 2026 14:43:58 +0200 Subject: [PATCH] feat: seed ad-hoc memory extension instructions (#20606) ## Summary Ad-hoc memory notes are written under `memories/extensions/ad_hoc/`, but the consolidation agent only knows how to interpret an extension when the extension folder has an `instructions.md`. Seed those instructions from the memories write pipeline so an enabled memories startup creates the expected ad-hoc extension layout automatically. This also moves extension-specific write behavior behind a dedicated `memories/write/src/extensions/` module. `ad_hoc` owns the seeded instructions template, while the existing resource-retention cleanup lives in its own `prune` module so future memory extensions can add their own write-side setup without growing a flat helper file. ## Changes - Seed `memories/extensions/ad_hoc/instructions.md` during eligible memory startup without overwriting an existing file. - Store the ad-hoc instructions template under `memories/write/templates/extensions/ad_hoc/`, keeping ownership in `codex-memories-write`. - Split memory extension support into `extensions::ad_hoc` and `extensions::prune`. - Keep the existing old-resource pruning behavior unchanged. ## Verification - `cargo test -p codex-memories-write` - `bazel build //codex-rs/memories/write:write` --------- Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> --- .../memories/write/src/extensions/ad_hoc.rs | 28 +++++++++++++++ .../write/src/extensions/ad_hoc_tests.rs | 36 +++++++++++++++++++ codex-rs/memories/write/src/extensions/mod.rs | 10 ++++++ .../{extensions.rs => extensions/prune.rs} | 2 +- .../prune_tests.rs} | 1 + codex-rs/memories/write/src/start.rs | 7 ++++ .../extensions}/ad_hoc/instructions.md | 0 7 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 codex-rs/memories/write/src/extensions/ad_hoc.rs create mode 100644 codex-rs/memories/write/src/extensions/ad_hoc_tests.rs create mode 100644 codex-rs/memories/write/src/extensions/mod.rs rename codex-rs/memories/write/src/{extensions.rs => extensions/prune.rs} (99%) rename codex-rs/memories/write/src/{extensions_tests.rs => extensions/prune_tests.rs} (98%) rename codex-rs/memories/{read/templates => write/templates/extensions}/ad_hoc/instructions.md (100%) diff --git a/codex-rs/memories/write/src/extensions/ad_hoc.rs b/codex-rs/memories/write/src/extensions/ad_hoc.rs new file mode 100644 index 000000000..9e77ba3ba --- /dev/null +++ b/codex-rs/memories/write/src/extensions/ad_hoc.rs @@ -0,0 +1,28 @@ +use crate::memory_extensions_root; +use std::path::Path; + +pub(super) const INSTRUCTIONS: &str = + include_str!("../../templates/extensions/ad_hoc/instructions.md"); + +pub(super) async fn seed_instructions(memory_root: &Path) -> std::io::Result<()> { + let extension_root = memory_extensions_root(memory_root).join("ad_hoc"); + let instructions_path = extension_root.join("instructions.md"); + + tokio::fs::create_dir_all(&extension_root).await?; + match tokio::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&instructions_path) + .await + { + Ok(mut file) => { + tokio::io::AsyncWriteExt::write_all(&mut file, INSTRUCTIONS.as_bytes()).await + } + Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), + Err(err) => Err(err), + } +} + +#[cfg(test)] +#[path = "ad_hoc_tests.rs"] +mod tests; diff --git a/codex-rs/memories/write/src/extensions/ad_hoc_tests.rs b/codex-rs/memories/write/src/extensions/ad_hoc_tests.rs new file mode 100644 index 000000000..7533f5ed1 --- /dev/null +++ b/codex-rs/memories/write/src/extensions/ad_hoc_tests.rs @@ -0,0 +1,36 @@ +use super::*; +use crate::memory_extensions_root; +use pretty_assertions::assert_eq; +use tempfile::TempDir; + +#[tokio::test] +async fn seeds_instructions_without_overwriting_existing_file() { + let codex_home = TempDir::new().expect("create temp codex home"); + let memory_root = codex_home.path().join("memories"); + let instructions_path = memory_extensions_root(&memory_root).join("ad_hoc/instructions.md"); + + seed_instructions(&memory_root) + .await + .expect("seed ad-hoc instructions"); + + assert_eq!( + tokio::fs::read_to_string(&instructions_path) + .await + .expect("read seeded ad-hoc instructions"), + INSTRUCTIONS + ); + + tokio::fs::write(&instructions_path, "custom instructions") + .await + .expect("write custom instructions"); + seed_instructions(&memory_root) + .await + .expect("seed ad-hoc instructions again"); + + assert_eq!( + tokio::fs::read_to_string(&instructions_path) + .await + .expect("read custom ad-hoc instructions"), + "custom instructions" + ); +} diff --git a/codex-rs/memories/write/src/extensions/mod.rs b/codex-rs/memories/write/src/extensions/mod.rs new file mode 100644 index 000000000..fdf26c887 --- /dev/null +++ b/codex-rs/memories/write/src/extensions/mod.rs @@ -0,0 +1,10 @@ +mod ad_hoc; +mod prune; + +use std::path::Path; + +pub(crate) async fn seed_extension_instructions(memory_root: &Path) -> std::io::Result<()> { + ad_hoc::seed_instructions(memory_root).await +} + +pub use prune::prune_old_extension_resources; diff --git a/codex-rs/memories/write/src/extensions.rs b/codex-rs/memories/write/src/extensions/prune.rs similarity index 99% rename from codex-rs/memories/write/src/extensions.rs rename to codex-rs/memories/write/src/extensions/prune.rs index 7b770cdf0..08ed1de17 100644 --- a/codex-rs/memories/write/src/extensions.rs +++ b/codex-rs/memories/write/src/extensions/prune.rs @@ -96,5 +96,5 @@ fn resource_timestamp(file_name: &str) -> Option> { } #[cfg(test)] -#[path = "extensions_tests.rs"] +#[path = "prune_tests.rs"] mod tests; diff --git a/codex-rs/memories/write/src/extensions_tests.rs b/codex-rs/memories/write/src/extensions/prune_tests.rs similarity index 98% rename from codex-rs/memories/write/src/extensions_tests.rs rename to codex-rs/memories/write/src/extensions/prune_tests.rs index e93335e16..ee70ba1af 100644 --- a/codex-rs/memories/write/src/extensions_tests.rs +++ b/codex-rs/memories/write/src/extensions/prune_tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::memory_extensions_root; use pretty_assertions::assert_eq; use tempfile::TempDir; diff --git a/codex-rs/memories/write/src/start.rs b/codex-rs/memories/write/src/start.rs index f7bf11e6f..007f5f8bb 100644 --- a/codex-rs/memories/write/src/start.rs +++ b/codex-rs/memories/write/src/start.rs @@ -1,4 +1,6 @@ +use crate::extensions::seed_extension_instructions; use crate::guard; +use crate::memory_root; use crate::metrics::MEMORY_STARTUP; use crate::phase1; use crate::phase2; @@ -47,6 +49,11 @@ pub fn start_memories_startup_task( } tokio::spawn(async move { + let root = memory_root(&config.codex_home); + if let Err(err) = seed_extension_instructions(&root).await { + warn!("failed seeding memory extension instructions: {err}"); + } + // Clean memories to make preserve DB size. This does not consume tokens so can be // done before the quota check. phase1::prune(context.as_ref(), &config).await; diff --git a/codex-rs/memories/read/templates/ad_hoc/instructions.md b/codex-rs/memories/write/templates/extensions/ad_hoc/instructions.md similarity index 100% rename from codex-rs/memories/read/templates/ad_hoc/instructions.md rename to codex-rs/memories/write/templates/extensions/ad_hoc/instructions.md