chore: move memory prompt builder into extension (#24558)

## Why

The memories extension now owns the read-path developer instructions it
injects at thread start. Keeping that prompt builder and template in
`codex-memories-read` left the extension depending on a helper crate for
extension-specific prompt assembly, and kept async template/truncation
dependencies in the read crate after the remaining read surface no
longer needed them.

## What changed

- Moved `prompts.rs`, its tests, and `templates/memories/read_path.md`
from `memories/read` into `ext/memories`.
- Wired `MemoryExtension` to call the local prompt builder and added the
moved templates to `ext/memories/BUILD.bazel` compile data.
- Removed the now-unused prompt export and prompt-related dependencies
from `codex-memories-read`.

## Testing

- Not run locally.
This commit is contained in:
jif-oai
2026-05-26 11:53:47 +02:00
committed by GitHub
parent d579dafb70
commit de513a83f3
11 changed files with 11 additions and 24 deletions
+3
View File
@@ -3,4 +3,7 @@ load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "memories",
crate_name = "codex_memories_extension",
compile_data = glob([
"templates/**",
]),
)
+1 -1
View File
@@ -17,10 +17,10 @@ async-trait = { workspace = true }
codex-core = { workspace = true }
codex-extension-api = { workspace = true }
codex-features = { workspace = true }
codex-memories-read = { workspace = true }
codex-tools = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-output-truncation = { workspace = true }
codex-utils-template = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
+1 -1
View File
@@ -10,10 +10,10 @@ use codex_extension_api::ThreadLifecycleContributor;
use codex_extension_api::ThreadStartInput;
use codex_extension_api::ToolContributor;
use codex_features::Feature;
use codex_memories_read::build_memory_tool_developer_instructions;
use codex_utils_absolute_path::AbsolutePathBuf;
use crate::local::LocalMemoriesBackend;
use crate::prompts::build_memory_tool_developer_instructions;
use crate::tools;
/// Contributes Codex memory read-path prompt context and memory read tools.
+2
View File
@@ -1,6 +1,7 @@
mod backend;
mod extension;
mod local;
mod prompts;
mod schema;
mod tools;
@@ -11,6 +12,7 @@ pub(crate) const MAX_LIST_RESULTS: usize = 2_000;
pub(crate) const DEFAULT_SEARCH_MAX_RESULTS: usize = 200;
pub(crate) const MAX_SEARCH_RESULTS: usize = 200;
pub(crate) const DEFAULT_READ_MAX_TOKENS: usize = 20_000;
pub(crate) const MEMORY_TOOL_DEVELOPER_INSTRUCTIONS_SUMMARY_TOKEN_LIMIT: usize = 2_500;
pub(crate) const MEMORY_TOOLS_NAMESPACE: &str = "memories/";
pub(crate) const LIST_TOOL_NAME: &str = "list";
+55
View File
@@ -0,0 +1,55 @@
use crate::MEMORY_TOOL_DEVELOPER_INSTRUCTIONS_SUMMARY_TOKEN_LIMIT;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_output_truncation::TruncationPolicy;
use codex_utils_output_truncation::truncate_text;
use codex_utils_template::Template;
use std::sync::LazyLock;
use tokio::fs;
static MEMORY_TOOL_DEVELOPER_INSTRUCTIONS_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
parse_embedded_template(
include_str!("../templates/memories/read_path.md"),
"memories/read_path.md",
)
});
fn parse_embedded_template(source: &'static str, template_name: &str) -> Template {
match Template::parse(source) {
Ok(template) => template,
Err(err) => panic!("embedded template {template_name} is invalid: {err}"),
}
}
/// Build the memory read-path prompt that is added to developer instructions.
///
/// Large `memory_summary.md` files are truncated at
/// [MEMORY_TOOL_DEVELOPER_INSTRUCTIONS_SUMMARY_TOKEN_LIMIT].
pub(crate) async fn build_memory_tool_developer_instructions(
codex_home: &AbsolutePathBuf,
) -> Option<String> {
let base_path = codex_home.join("memories");
let memory_summary_path = base_path.join("memory_summary.md");
let memory_summary = fs::read_to_string(&memory_summary_path)
.await
.ok()?
.trim()
.to_string();
let memory_summary = truncate_text(
&memory_summary,
TruncationPolicy::Tokens(MEMORY_TOOL_DEVELOPER_INSTRUCTIONS_SUMMARY_TOKEN_LIMIT),
);
if memory_summary.is_empty() {
return None;
}
let base_path = base_path.display().to_string();
MEMORY_TOOL_DEVELOPER_INSTRUCTIONS_TEMPLATE
.render([
("base_path", base_path.as_str()),
("memory_summary", memory_summary.as_str()),
])
.ok()
}
#[cfg(test)]
#[path = "prompts_tests.rs"]
mod tests;
@@ -0,0 +1,35 @@
use super::*;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
use tokio::fs as tokio_fs;
#[tokio::test]
async fn build_memory_tool_developer_instructions_renders_embedded_template() {
let temp = tempdir().unwrap();
let codex_home = AbsolutePathBuf::from_absolute_path(temp.path()).unwrap();
let memories_dir = codex_home.join("memories");
tokio_fs::create_dir_all(&memories_dir).await.unwrap();
tokio_fs::write(
memories_dir.join("memory_summary.md"),
"Short memory summary for tests.",
)
.await
.unwrap();
let instructions = build_memory_tool_developer_instructions(&codex_home)
.await
.unwrap();
assert!(instructions.contains(&format!(
"- {}/memory_summary.md (already provided below; do NOT open again)",
memories_dir.display()
)));
assert!(instructions.contains("Short memory summary for tests."));
assert_eq!(
instructions
.matches("========= MEMORY_SUMMARY BEGINS =========")
.count(),
1
);
}
@@ -0,0 +1,130 @@
## Memory
You have access to a memory folder with guidance from prior runs. It can save
time and help you stay consistent. Use it whenever it is likely to help.
Decision boundary: should you use memory for a new user query?
- Skip memory ONLY when the request is clearly self-contained and does not need
workspace history, conventions, or prior decisions.
- Hard skip examples: current time/date, simple translation, simple sentence
rewrite, one-line shell command, trivial formatting.
- Use memory by default when ANY of these are true:
- the query mentions workspace/repo/module/path/files in MEMORY_SUMMARY below,
- the user asks for prior context / consistency / previous decisions,
- the task is ambiguous and could depend on earlier project choices,
- the ask is a non-trivial and related to MEMORY_SUMMARY below.
- If unsure, do a quick memory pass.
Memory layout (general -> specific):
- {{ base_path }}/memory_summary.md (already provided below; do NOT open again)
- {{ base_path }}/MEMORY.md (searchable registry; primary file to query)
- {{ base_path }}/skills/<skill-name>/ (skill folder)
- SKILL.md (entrypoint instructions)
- scripts/ (optional helper scripts)
- examples/ (optional example outputs)
- templates/ (optional templates)
- {{ base_path }}/rollout_summaries/ (per-rollout recaps + evidence snippets)
- The paths of these entries can be found in {{ base_path }}/MEMORY.md or {{ base_path }}/rollout_summaries/ as `rollout_path`
- These files are append-only `jsonl`: `session_meta.payload.id` identifies the session, `turn_context` marks turn boundaries, `event_msg` is the lightweight status stream, and `response_item` contains actual messages, tool calls, and tool outputs.
- For efficient lookup, prefer matching the filename suffix or `session_meta.payload.id`; avoid broad full-content scans unless needed.
Quick memory pass (when applicable):
1. Skim the MEMORY_SUMMARY below and extract task-relevant keywords.
2. Search {{ base_path }}/MEMORY.md using those keywords.
3. Only if MEMORY.md directly points to rollout summaries/skills, open the 1-2
most relevant files under {{ base_path }}/rollout_summaries/ or
{{ base_path }}/skills/.
4. If above are not clear and you need exact commands, error text, or precise evidence, search over `rollout_path` for more evidence.
5. If there are no relevant hits, stop memory lookup and continue normally.
Quick-pass budget:
- Keep memory lookup lightweight: ideally <= 4-6 search steps before main work.
- Avoid broad scans of all rollout summaries.
During execution: if you hit repeated errors, confusing behavior, or suspect
relevant prior context, redo the quick memory pass.
How to decide whether to verify memory:
- Consider both risk of drift and verification effort.
- If a fact is likely to drift and is cheap to verify, verify it before
answering.
- If a fact is likely to drift but verification is expensive, slow, or
disruptive, it is acceptable to answer from memory in an interactive turn,
but you should say that it is memory-derived, note that it may be stale, and
consider offering to refresh it live.
- If a fact is lower-drift and expensive to verify, it is usually fine to
answer from memory directly.
When answering from memory without current verification:
- If you rely on memory for a fact that you did not verify in the current turn,
say so briefly in the final answer.
- If that fact is plausibly drift-prone or comes from an older note, older
snapshot, or prior run summary, say that it may be stale or outdated.
- If live verification was skipped and a refresh would be useful in the
interactive context, consider offering to verify or refresh it live.
- Do not present unverified memory-derived facts as confirmed-current.
- Prefer a short refresh offer for interactive questions, especially about prior
results, commands, timing, or older snapshots.
Memory citation requirements:
- If ANY relevant memory files were used: append exactly one
`<oai-mem-citation>` block as the VERY LAST content of the final reply.
Normal responses should include the answer first, then append the
`<oai-mem-citation>` block at the end.
- Use this exact structure for programmatic parsing:
```
<oai-mem-citation>
<citation_entries>
MEMORY.md:234-236|note=[responsesapi citation extraction code pointer]
rollout_summaries/2026-02-17T21-23-02-LN3m-example.md:10-12|note=[weekly report format]
</citation_entries>
<rollout_ids>
019c6e27-e55b-73d1-87d8-4e01f1f75043
019c7714-3b77-74d1-9866-e1f484aae2ab
</rollout_ids>
</oai-mem-citation>
```
- `citation_entries` is for rendering:
- one citation entry per line
- format: `<file>:<line_start>-<line_end>|note=[<how memory was used>]`
- use file paths relative to the memory base path (for example, `MEMORY.md`,
`rollout_summaries/...`, `skills/...`)
- only cite files actually used under the memory base path (do not cite
workspace files as memory citations)
- if you used `MEMORY.md` and then a rollout summary/skill file, cite both
- list entries in order of importance (most important first)
- `note` should be short, single-line, and use simple characters only (avoid
unusual symbols, no newlines)
- `rollout_ids` is for us to track what previous rollouts you find useful:
- include one rollout id per line
- rollout ids should look like UUIDs (for example,
`019c6e27-e55b-73d1-87d8-4e01f1f75043`)
- include unique ids only; do not repeat ids
- an empty `<rollout_ids>` section is allowed if no rollout ids are available
- you can find rollout ids in rollout summary files and MEMORY.md
- do not include file paths or notes in this section
- For every `citation_entries`, try to find and cite the corresponding rollout id if possible
- Never include memory citations inside pull-request messages.
- Never cite blank lines; double-check ranges.
Updating memories:
You can update the memories **only** when explicitly asked by the user. This must always come from a direct request from the user.
- Write your update in {{ base_path }}/extensions/ad_hoc/notes/
- Each update must be one small file containing what you want to add/delete/update from the memories.
- The name of this file must be `<timestamp>-<short slug>.md`
- Do not try to edit the memory files yourself, only add one update note in {{ base_path }}/extensions/ad_hoc/notes/
========= MEMORY_SUMMARY BEGINS =========
{{ memory_summary }}
========= MEMORY_SUMMARY ENDS =========
When memory is likely relevant, start with the quick memory pass above before
deep repo exploration.