Files
codex/codex-rs/memories/write/src/start.rs

113 lines
4.3 KiB
Rust

use crate::ensure_layout;
use crate::extensions::seed_extension_instructions;
use crate::guard;
use crate::metrics::MEMORY_STARTUP;
use crate::phase1;
use crate::phase2;
use crate::runtime::MemoryStartupContext;
use codex_core::CodexThread;
use codex_core::ThreadManager;
use codex_core::config::Config;
use codex_features::Feature;
use codex_login::AuthManager;
use codex_protocol::MemoryVersion;
use codex_protocol::ThreadId;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::SessionSource;
use std::sync::Arc;
use tracing::warn;
/// Starts the asynchronous startup memory pipeline for an eligible root session.
///
/// The pipeline is skipped for ephemeral sessions, disabled feature flags, and
/// subagent sessions.
pub fn start_memories_startup_task(
thread_manager: Arc<ThreadManager>,
auth_manager: Arc<AuthManager>,
thread_id: ThreadId,
thread: Arc<CodexThread>,
config: Arc<Config>,
parent_permission_profile: PermissionProfile,
source: &SessionSource,
) {
if config.ephemeral
|| !config.features.enabled(Feature::MemoryTool)
|| source.is_non_root_agent()
{
return;
}
let versions = if config.memories.dual_write {
vec![MemoryVersion::V1, MemoryVersion::V2]
} else {
vec![config.memories.version]
};
for version in versions {
let mut pipeline_config = config.as_ref().clone();
pipeline_config.memories.version = version;
// Background learning may use a local provider even when the foreground
// session uses a different model. Resolve before constructing either the
// extraction client or the consolidation worker, so BOTH use this route.
// A missing route must never silently buy a foreground-provider call.
if let Some(id) = pipeline_config.memories.model_provider.clone() {
let Some(provider) = pipeline_config.model_providers.get(&id).cloned() else {
warn!(provider = %id, "memory provider is not configured; skipping background learning");
continue;
};
if pipeline_config.memories.extract_model.is_none()
|| pipeline_config.memories.consolidation_model.is_none()
{
warn!(provider = %id, "independent memory provider needs explicit extraction and consolidation models; skipping");
continue;
}
pipeline_config.model_provider_id = id;
pipeline_config.model_provider = provider;
}
let config = Arc::new(pipeline_config);
let auth_manager = Arc::clone(&auth_manager);
let parent_permission_profile = parent_permission_profile.clone();
let context = Arc::new(MemoryStartupContext::new(
Arc::clone(&thread_manager),
Arc::clone(&auth_manager),
thread_id,
Arc::clone(&thread),
config.as_ref(),
source.clone(),
));
tokio::spawn(async move {
if context.memory_store().await.is_none() {
warn!("state db unavailable for memories startup pipeline; skipping");
return;
}
let root = config
.codex_home
.join(config.memories.version.directory_name());
if let Err(err) = ensure_layout(&root).await {
warn!("failed preparing memories root: {err}");
return;
}
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;
if !guard::rate_limits_ok(&auth_manager, &config).await {
context.counter(
MEMORY_STARTUP,
/*inc*/ 1,
&[("status", "skipped_rate_limit")],
);
return;
}
// Run phase 1.
phase1::run(Arc::clone(&context), Arc::clone(&config)).await;
// Run phase 2.
phase2::run(context, config, parent_permission_profile).await;
});
}
}