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>
This commit is contained in:
jif-oai
2026-05-01 14:43:58 +02:00
committed by GitHub
Unverified
parent 70fc55b8f3
commit ff27d01676
7 changed files with 83 additions and 1 deletions
@@ -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;
@@ -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"
);
}
@@ -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;
@@ -96,5 +96,5 @@ fn resource_timestamp(file_name: &str) -> Option<DateTime<Utc>> {
}
#[cfg(test)]
#[path = "extensions_tests.rs"]
#[path = "prune_tests.rs"]
mod tests;
@@ -1,4 +1,5 @@
use super::*;
use crate::memory_extensions_root;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
+7
View File
@@ -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;