Support memory v2 in external-agent memory imports (#56414)

## Why

External-agent memory imports previously wrote only to the v1 workspace and queued only v1 consolidation, leaving the v2 pipeline without imported resources.

## What changed

- Always import selected resources into `memories_v2`. Also update `memories` when v1 is selected or a legacy workspace exists.
- Read `memories.version` for detection and import, detect pending changes across the applicable namespaces, and enqueue consolidation separately for each updated namespace.
- Combine project results across namespaces so cleanup in either can succeed while failures in either are reported.
- Adapt imported-memory consolidation instructions to the active pipeline's output format.

## Testing

Expand app-server integration coverage for v2 profiles, explicit v1 selection, default profiles, and existing legacy workspaces. Verify resource copies, consolidation queues, repeat detection, and cleanup of deleted or unscoped projects.

GitOrigin-RevId: 7e9206942d63fd55b7f2a7f5f9210b687da6a973
This commit is contained in:
jif
2026-10-23 09:06:13 +00:00
committed by copyberry
parent d6817454d7
commit e6ebdfe00b
8 changed files with 259 additions and 109 deletions
@@ -1,3 +1,4 @@
use codex_protocol::MemoryVersion;
use std::sync::Arc;
use std::time::Duration;
@@ -126,9 +127,11 @@ impl ExternalAgentConfigRequestProcessor {
&self,
params: ExternalAgentConfigDetectParams,
) -> Result<ExternalAgentConfigDetectResponse, JSONRPCErrorError> {
let (include_memory, memory_version) = self.memory_import_settings().await;
let migration_service = self
.migration_service
.with_migration_source(params.migration_source.as_deref());
.with_migration_source(params.migration_source.as_deref())
.with_memory_version(memory_version);
let default_session_import_limits = ExternalAgentSessionImportLimits::default();
let migration_service =
migration_service.with_session_import_limits(ExternalAgentSessionImportLimits {
@@ -143,7 +146,7 @@ impl ExternalAgentConfigRequestProcessor {
});
let options = ExternalAgentConfigDetectOptions {
include_home: params.include_home,
include_memory: self.external_agent_memory_import_enabled().await,
include_memory,
cwds: params.cwds,
};
let items = migration_service
@@ -175,11 +178,12 @@ impl ExternalAgentConfigRequestProcessor {
request_id: ConnectionRequestId,
params: ExternalAgentConfigImportParams,
) -> Result<(), JSONRPCErrorError> {
let (include_memory, memory_version) = self.memory_import_settings().await;
if params
.migration_items
.iter()
.any(|item| item.item_type == ExternalAgentConfigMigrationItemType::Memory)
&& !self.external_agent_memory_import_enabled().await
&& !include_memory
{
return Err(invalid_request("external agent memory import is disabled"));
}
@@ -199,7 +203,8 @@ impl ExternalAgentConfigRequestProcessor {
let provider_id = params.provider_id.clone();
let migration_service = self
.migration_service
.with_migration_source(params.migration_source.as_deref());
.with_migration_source(params.migration_source.as_deref())
.with_memory_version(memory_version);
let needs_runtime_refresh = migration_items_need_runtime_refresh(&params.migration_items);
let has_migration_items = !params.migration_items.is_empty();
let has_plugin_imports = params.migration_items.iter().any(|item| {
@@ -355,7 +360,7 @@ impl ExternalAgentConfigRequestProcessor {
Ok(())
}
async fn external_agent_memory_import_enabled(&self) -> bool {
async fn memory_import_settings(&self) -> (bool, MemoryVersion) {
let config = match self
.config_manager
.load_latest_config(/*fallback_cwd*/ None)
@@ -367,10 +372,13 @@ impl ExternalAgentConfigRequestProcessor {
error = %err,
"failed to reload config for external agent memory import detection"
);
return false;
return (false, MemoryVersion::V1);
}
};
config.features.enabled(Feature::ExternalAgentMemoryImport)
(
config.features.enabled(Feature::ExternalAgentMemoryImport),
config.memories.version,
)
}
pub(crate) async fn read_import_histories(