Add configurable memory versions with isolated storage (#43797)

## What changed

- Add `memories.version` with `v1` as the default and `v2` as an opt-in selection.
- Route memory generation, summaries, and retrieval tools through the selected version. Store `v2` artifacts in `memories_v2` and lazily create a separate SQLite database, keeping jobs and outputs isolated while sharing the thread catalog.
- Preserve the initial memory version across extension config updates so summaries and retrieval tools use the same namespace.
- Clear both versions on memory reset and remove thread memory from both stores on thread deletion. Recognize both artifact roots in shell usage telemetry.

## Testing

Add regression coverage for version defaults and validation, independent job claims and outputs, deletion and reset across versions, and shell usage classification for both memory roots.

GitOrigin-RevId: aa799bf87ab6ec10c8f5668213931e57547d080b
This commit is contained in:
jif
2026-09-08 10:55:48 +00:00
committed by copyberry
parent 35d9e4bc4d
commit 3f76e88a48
27 changed files with 422 additions and 119 deletions
+45 -11
View File
@@ -44,6 +44,7 @@ mod external_agent_config_imports;
mod goals;
mod logs;
mod memories;
mod memory_versions;
mod projects;
mod queued_items;
mod recovery;
@@ -92,6 +93,7 @@ pub struct StateRuntime {
logs_pool: Arc<sqlx::SqlitePool>,
thread_goals: GoalStore,
memories: MemoryStore,
memories_v2: Arc<tokio::sync::OnceCell<MemoryStore>>,
thread_queue: SqliteQueueStore,
thread_updated_at_millis: Arc<AtomicI64>,
thread_recency_at_millis: Arc<AtomicI64>,
@@ -133,6 +135,7 @@ impl StateRuntime {
let goals_path = sqlite.goals_db_path();
let memories_path = sqlite.memories_db_path();
let queue_path = sqlite.queue_db_path();
let has_memories_v2 = tokio::fs::try_exists(sqlite.memories_v2_db_path()).await?;
let pool = match sqlite
.open_state_db(&state_migrator, telemetry_override)
.await
@@ -251,6 +254,7 @@ impl StateRuntime {
let runtime = Arc::new(Self {
thread_goals: GoalStore::new(Arc::clone(&goals_pool)),
memories: MemoryStore::new(Arc::clone(&memories_pool), Arc::clone(&pool)),
memories_v2: Arc::new(tokio::sync::OnceCell::new()),
thread_queue: SqliteQueueStore::new(queue_pool),
pool,
logs_pool,
@@ -259,6 +263,16 @@ impl StateRuntime {
thread_updated_at_millis: Arc::new(AtomicI64::new(thread_updated_at_millis)),
thread_recency_at_millis: Arc::new(AtomicI64::new(thread_recency_at_millis)),
});
// Existing v2 state must participate in startup corruption recovery.
// Keep creation lazy for users who have never used v2.
if has_memories_v2
&& let Err(err) = runtime
.memories_for_version(codex_protocol::MemoryVersion::V2)
.await
{
runtime.close().await;
return Err(err);
}
if let Err(err) = runtime.run_logs_startup_maintenance().await {
warn!(
"failed to run startup maintenance for logs db at {}: {err}",
@@ -290,24 +304,44 @@ impl StateRuntime {
pub async fn close(&self) {
self.thread_queue.close().await;
self.memories.close().await;
if let Some(memories) = self.memories_v2.get() {
memories.close().await;
}
self.thread_goals.close().await;
self.logs_pool.close().await;
self.pool.close().await;
}
pub async fn clear_memory_data_in_sqlite_home(sqlite: &SqliteConfig) -> anyhow::Result<bool> {
let memories_path = sqlite.memories_db_path();
if !tokio::fs::try_exists(&memories_path).await? {
return Ok(false);
let mut cleared = false;
for version in [
codex_protocol::MemoryVersion::V1,
codex_protocol::MemoryVersion::V2,
] {
let path = match version {
codex_protocol::MemoryVersion::V1 => sqlite.memories_db_path(),
codex_protocol::MemoryVersion::V2 => sqlite.memories_v2_db_path(),
};
if !tokio::fs::try_exists(path).await? {
continue;
}
let pool = match version {
codex_protocol::MemoryVersion::V1 => {
sqlite
.open_memories_db(
&runtime_memories_migrator(),
/*telemetry_override*/ None,
)
.await?
}
codex_protocol::MemoryVersion::V2 => sqlite.open_memories_v2_db().await?,
};
let result = memories::clear_memory_data_in_pool(&pool).await;
pool.close().await;
result?;
cleared = true;
}
let memories_migrator = runtime_memories_migrator();
let pool = sqlite
.open_memories_db(&memories_migrator, /*telemetry_override*/ None)
.await?;
memories::clear_memory_data_in_pool(&pool).await?;
pool.close().await;
Ok(true)
Ok(cleared)
}
}