mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
Add experimental thread memory summary reads (#56465)
## What changed
Add `thread/memory/read` to return `{summary}` for a thread that has been started or resumed. Read `memory_summary.md` from the thread's resolved memory version, honoring thread-specific configuration overrides without falling back to another version.
Return `null` when memory use is disabled or the selected summary is missing, unreadable, or blank.
## Testing
Extend app-server integration coverage to check explicit and automatic memory version selection, disabled memory use, and missing summaries without fallback.
GitOrigin-RevId: b218f31e379c69ccbacb444bb9d6ee920cd4882b
This commit is contained in:
BIN
Binary file not shown.
@@ -720,6 +720,12 @@ client_request_definitions! {
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadMemoryModeSetResponse,
|
||||
},
|
||||
#[experimental("thread/memory/read")]
|
||||
ThreadMemoryRead => "thread/memory/read" {
|
||||
params: v2::ThreadMemoryReadParams,
|
||||
serialization: thread_id(params.thread_id),
|
||||
response: v2::ThreadMemoryReadResponse,
|
||||
},
|
||||
#[experimental("memory/status")]
|
||||
MemoryStatus => "memory/status" {
|
||||
params: v2::MemoryStatusParams,
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
//! Readiness for selecting v2 memory context after background consolidation.
|
||||
//! Memory readiness and summary reads using a loaded thread's selected pipeline.
|
||||
|
||||
use crate::JsonSchema;
|
||||
use crate::TS;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadMemoryReadParams {
|
||||
/// The thread must already be started or resumed.
|
||||
pub thread_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadMemoryReadResponse {
|
||||
/// Summary from the thread's selected memory version. Null when memory use is
|
||||
/// disabled or the selected summary is missing, unreadable, or blank.
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
# Thread memory summary (experimental)
|
||||
|
||||
`thread/memory/read` takes `{threadId}` for a loaded thread and returns
|
||||
`{summary}`. Start or resume the thread first. The summary uses that thread's
|
||||
resolved memory version, including thread-specific configuration overrides.
|
||||
It is `null` when memory use is disabled or the selected summary is missing,
|
||||
unreadable, or blank. It never falls back to the other memory version.
|
||||
|
||||
# Thread list exclusions
|
||||
|
||||
`thread/list` accepts `excludedThreadIds`, an optional array of up to 100
|
||||
|
||||
@@ -1472,6 +1472,9 @@ impl MessageProcessor {
|
||||
ClientRequest::MemoryStatus { params, .. } => {
|
||||
self.thread_processor.memory_status(params).await
|
||||
}
|
||||
ClientRequest::ThreadMemoryRead { params, .. } => {
|
||||
self.thread_processor.thread_memory_read(params).await
|
||||
}
|
||||
ClientRequest::MemoryReset { .. } => self.thread_processor.memory_reset().await,
|
||||
ClientRequest::RolloutCompress { .. } => self.thread_processor.rollout_compress(),
|
||||
ClientRequest::ThreadUnarchive { params, .. } => {
|
||||
|
||||
@@ -570,6 +570,7 @@ mod thread_attachments;
|
||||
mod thread_enrichment;
|
||||
mod thread_fork_goal;
|
||||
mod thread_input;
|
||||
mod thread_memory;
|
||||
mod thread_processor;
|
||||
mod thread_queue_processor;
|
||||
mod thread_read_state;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Reads memory context from the loaded thread's resolved version, without fallback.
|
||||
|
||||
use super::*;
|
||||
use codex_app_server_protocol::ThreadMemoryReadParams;
|
||||
use codex_app_server_protocol::ThreadMemoryReadResponse;
|
||||
|
||||
impl ThreadRequestProcessor {
|
||||
pub(crate) async fn thread_memory_read(
|
||||
&self,
|
||||
params: ThreadMemoryReadParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
let (_, thread) = self.load_thread(¶ms.thread_id).await?;
|
||||
let config = thread.config().await;
|
||||
let summary =
|
||||
if config.features.enabled(Feature::MemoryTool) && config.memories.use_memories {
|
||||
tokio::fs::read_to_string(
|
||||
config
|
||||
.codex_home
|
||||
.join(config.memories.version.directory_name())
|
||||
.join("memory_summary.md"),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.filter(|summary| !summary.trim().is_empty())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Some(ThreadMemoryReadResponse { summary }.into()))
|
||||
}
|
||||
}
|
||||
@@ -1016,7 +1016,7 @@ impl ThreadRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
async fn load_thread(
|
||||
pub(super) async fn load_thread(
|
||||
&self,
|
||||
thread_id: &str,
|
||||
) -> Result<(ThreadId, Arc<CodexThread>), JSONRPCErrorError> {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::MockResponsesConfig;
|
||||
use app_test_support::TestAppServer;
|
||||
use codex_app_server_protocol::ThreadMemoryReadResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
@@ -22,11 +23,30 @@ use tempfile::TempDir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn memory_read_version_selects_only_its_own_context() -> Result<()> {
|
||||
for (version, has_v1_workspace, expected, excluded) in [
|
||||
(Some("v1"), true, "legacy-only-marker", "v2-only-marker"),
|
||||
(Some("v2"), true, "v2-only-marker", "legacy-only-marker"),
|
||||
(None, true, "legacy-only-marker", "v2-only-marker"),
|
||||
(None, false, "v2-only-marker", "legacy-only-marker"),
|
||||
for (version, has_v1_workspace, expected, excluded, use_memories) in [
|
||||
(
|
||||
Some("v1"),
|
||||
true,
|
||||
"legacy-only-marker",
|
||||
"v2-only-marker",
|
||||
true,
|
||||
),
|
||||
(
|
||||
Some("v2"),
|
||||
true,
|
||||
"v2-only-marker",
|
||||
"legacy-only-marker",
|
||||
true,
|
||||
),
|
||||
(None, true, "legacy-only-marker", "v2-only-marker", true),
|
||||
(None, false, "v2-only-marker", "legacy-only-marker", true),
|
||||
(
|
||||
Some("v2"),
|
||||
true,
|
||||
"v2-only-marker",
|
||||
"legacy-only-marker",
|
||||
false,
|
||||
),
|
||||
] {
|
||||
let server = start_mock_server().await;
|
||||
let home = TempDir::new()?;
|
||||
@@ -60,10 +80,10 @@ async fn memory_read_version_selects_only_its_own_context() -> Result<()> {
|
||||
.with_codex_home(home.path())
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let mut config = std::collections::HashMap::from([(
|
||||
"memories.generate_memories".to_string(),
|
||||
json!(false),
|
||||
)]);
|
||||
let mut config = std::collections::HashMap::from([
|
||||
("memories.generate_memories".to_string(), json!(false)),
|
||||
("memories.use_memories".to_string(), json!(use_memories)),
|
||||
]);
|
||||
if let Some(version) = version {
|
||||
config.insert("memories.version".to_string(), json!(version));
|
||||
}
|
||||
@@ -76,8 +96,21 @@ async fn memory_read_version_selects_only_its_own_context() -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
let response: ThreadStartResponse = app.read_response(start).await?;
|
||||
let summary_request = app
|
||||
.send_raw_request(
|
||||
"thread/memory/read",
|
||||
Some(json!({ "threadId": response.thread.id })),
|
||||
)
|
||||
.await?;
|
||||
let summary: ThreadMemoryReadResponse = app.read_response(summary_request).await?;
|
||||
assert_eq!(
|
||||
summary,
|
||||
ThreadMemoryReadResponse {
|
||||
summary: use_memories.then(|| format!("v1\n{expected}\n")),
|
||||
}
|
||||
);
|
||||
app.send_turn_start_request(TurnStartParams {
|
||||
thread_id: response.thread.id,
|
||||
thread_id: response.thread.id.clone(),
|
||||
input: vec![UserInput::Text {
|
||||
text: "Use my memory preferences".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
@@ -88,8 +121,23 @@ async fn memory_read_version_selects_only_its_own_context() -> Result<()> {
|
||||
let completed: TurnCompletedNotification = app.read_notification("turn/completed").await?;
|
||||
assert_eq!(completed.turn.status, TurnStatus::Completed);
|
||||
let request = mock.single_request();
|
||||
assert!(request.body_contains_text(expected));
|
||||
assert_eq!(request.body_contains_text(expected), use_memories);
|
||||
assert!(!request.body_contains_text(excluded));
|
||||
|
||||
let directory = if version == Some("v1") || (version.is_none() && has_v1_workspace) {
|
||||
"memories"
|
||||
} else {
|
||||
"memories_v2"
|
||||
};
|
||||
tokio::fs::remove_file(home.path().join(directory).join("memory_summary.md")).await?;
|
||||
let summary_request = app
|
||||
.send_raw_request(
|
||||
"thread/memory/read",
|
||||
Some(json!({ "threadId": response.thread.id })),
|
||||
)
|
||||
.await?;
|
||||
let summary: ThreadMemoryReadResponse = app.read_response(summary_request).await?;
|
||||
assert_eq!(summary, ThreadMemoryReadResponse { summary: None });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user