further cleanup

This commit is contained in:
jif-oai
2026-02-10 17:50:41 +00:00
parent e8ee5ec201
commit ba3a423c3d
14 changed files with 410 additions and 577 deletions
-7
View File
@@ -24,13 +24,6 @@ pub(super) fn memory_root_for_cwd(codex_home: &Path, cwd: &Path) -> PathBuf {
codex_home.join("memories").join(bucket).join(MEMORY_SUBDIR)
}
/// Returns the DB scope key for a cwd-scoped memory entry.
///
/// This uses the same normalization/fallback behavior as cwd bucket derivation.
pub(super) fn memory_scope_key_for_cwd(cwd: &Path) -> String {
normalize_cwd_for_memory(cwd).display().to_string()
}
/// Returns the on-disk user-shared memory root directory.
pub(super) fn memory_root_for_user(codex_home: &Path) -> PathBuf {
codex_home
-1
View File
@@ -8,7 +8,6 @@ mod layout;
mod prompts;
mod rollout;
mod scope;
mod selection;
mod stage_one;
mod startup;
mod storage;
-47
View File
@@ -1,47 +0,0 @@
use chrono::Duration;
use chrono::Utc;
use codex_protocol::ThreadId;
use codex_state::ThreadMetadata;
use super::types::RolloutCandidate;
/// Selects rollout candidates that need stage-1 memory extraction.
///
/// A rollout is selected when it is not the active thread and was updated
/// within the configured max age window.
pub(super) fn select_rollout_candidates_from_db(
items: &[ThreadMetadata],
current_thread_id: ThreadId,
max_items: usize,
max_age_days: i64,
) -> Vec<RolloutCandidate> {
if max_items == 0 {
return Vec::new();
}
let cutoff = Utc::now() - Duration::days(max_age_days.max(0));
let mut candidates = Vec::new();
for item in items {
if item.id == current_thread_id {
continue;
}
if item.updated_at < cutoff {
continue;
}
candidates.push(RolloutCandidate {
thread_id: item.id,
rollout_path: item.rollout_path.clone(),
cwd: item.cwd.clone(),
source_updated_at: item.updated_at.timestamp(),
});
if candidates.len() >= max_items {
break;
}
}
candidates
}
@@ -8,15 +8,15 @@ use tracing::debug;
use tracing::info;
use tracing::warn;
use super::super::super::MAX_RAW_MEMORIES_PER_SCOPE;
use super::super::super::MEMORY_CONSOLIDATION_SUBAGENT_LABEL;
use super::super::super::PHASE_TWO_JOB_LEASE_SECONDS;
use super::super::super::PHASE_TWO_JOB_RETRY_DELAY_SECONDS;
use super::super::super::prompts::build_consolidation_prompt;
use super::super::super::storage::rebuild_memory_summary_from_memories;
use super::super::super::storage::sync_raw_memories_from_memories;
use super::super::super::storage::wipe_consolidation_outputs;
use super::super::MemoryScopeTarget;
use super::super::MAX_RAW_MEMORIES_PER_SCOPE;
use super::super::MEMORY_CONSOLIDATION_SUBAGENT_LABEL;
use super::super::PHASE_TWO_JOB_LEASE_SECONDS;
use super::super::PHASE_TWO_JOB_RETRY_DELAY_SECONDS;
use super::super::prompts::build_consolidation_prompt;
use super::super::storage::rebuild_memory_summary_from_memories;
use super::super::storage::sync_raw_memories_from_memories;
use super::super::storage::wipe_consolidation_outputs;
use super::MemoryScopeTarget;
use super::watch::spawn_phase2_completion_task;
pub(super) async fn run_memory_consolidation_for_scope(
@@ -18,21 +18,21 @@ use crate::memories::rollout::serialize_filtered_rollout_response_items;
use crate::memories::stage_one::RAW_MEMORY_PROMPT;
use crate::memories::stage_one::parse_stage_one_output;
use crate::memories::stage_one::stage_one_output_schema;
use crate::memories::types::RolloutCandidate;
use crate::memories::types::StageOneOutput;
use std::path::Path;
pub(super) async fn extract_stage_one_output(
session: &Session,
candidate: &RolloutCandidate,
rollout_path: &Path,
stage_one_context: &StageOneRequestContext,
) -> Result<StageOneOutput, &'static str> {
let (rollout_items, _thread_id, parse_errors) =
match RolloutRecorder::load_rollout_items(&candidate.rollout_path).await {
match RolloutRecorder::load_rollout_items(rollout_path).await {
Ok(result) => result,
Err(err) => {
warn!(
"failed to load rollout {} for memories: {err}",
candidate.rollout_path.display()
rollout_path.display()
);
return Err("failed to load rollout");
}
@@ -40,7 +40,7 @@ pub(super) async fn extract_stage_one_output(
if parse_errors > 0 {
warn!(
"rollout {} had {parse_errors} parse errors while preparing stage-1 memory input",
candidate.rollout_path.display()
rollout_path.display()
);
}
@@ -52,7 +52,7 @@ pub(super) async fn extract_stage_one_output(
Err(err) => {
warn!(
"failed to prepare filtered rollout payload {} for memories: {err}",
candidate.rollout_path.display()
rollout_path.display()
);
return Err("failed to serialize filtered rollout");
}
@@ -63,7 +63,7 @@ pub(super) async fn extract_stage_one_output(
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: build_stage_one_input_message(&candidate.rollout_path, &rollout_contents),
text: build_stage_one_input_message(rollout_path, &rollout_contents),
}],
end_turn: None,
phase: None,
@@ -93,7 +93,7 @@ pub(super) async fn extract_stage_one_output(
Err(err) => {
warn!(
"stage-1 memory request failed for rollout {}: {err}",
candidate.rollout_path.display()
rollout_path.display()
);
return Err("stage-1 memory request failed");
}
@@ -104,7 +104,7 @@ pub(super) async fn extract_stage_one_output(
Err(err) => {
warn!(
"failed while waiting for stage-1 memory response for rollout {}: {err}",
candidate.rollout_path.display()
rollout_path.display()
);
return Err("stage-1 memory response stream failed");
}
@@ -115,7 +115,7 @@ pub(super) async fn extract_stage_one_output(
Err(err) => {
warn!(
"invalid stage-1 memory payload for rollout {}: {err}",
candidate.rollout_path.display()
rollout_path.display()
);
Err("invalid stage-1 memory payload")
}
+173 -26
View File
@@ -1,7 +1,9 @@
mod phase_one;
mod phase_two;
mod dispatch;
mod extract;
mod watch;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::config::Config;
use crate::error::Result as CodexResult;
use crate::features::Feature;
@@ -11,9 +13,13 @@ use crate::memories::scope::MEMORY_SCOPE_KEY_USER;
use crate::memories::scope::MEMORY_SCOPE_KIND_CWD;
use crate::memories::scope::MEMORY_SCOPE_KIND_USER;
use crate::rollout::INTERACTIVE_SESSION_SOURCES;
use crate::rollout::list::ThreadSortKey;
use crate::state_db;
use codex_otel::OtelManager;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::SessionSource;
use futures::StreamExt;
use serde_json::Value;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::info;
@@ -21,6 +27,27 @@ use tracing::warn;
pub(super) const PHASE_ONE_THREAD_SCAN_LIMIT: usize = 5_000;
#[derive(Clone)]
struct StageOneRequestContext {
model_info: ModelInfo,
otel_manager: OtelManager,
reasoning_effort: Option<ReasoningEffortConfig>,
reasoning_summary: ReasoningSummaryConfig,
turn_metadata_header: Option<String>,
}
impl StageOneRequestContext {
fn from_turn_context(turn_context: &TurnContext, turn_metadata_header: Option<String>) -> Self {
Self {
model_info: turn_context.model_info.clone(),
otel_manager: turn_context.otel_manager.clone(),
reasoning_effort: turn_context.reasoning_effort,
reasoning_summary: turn_context.reasoning_summary,
turn_metadata_header,
}
}
}
/// Canonical memory scope metadata used by both startup phases.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct MemoryScopeTarget {
@@ -112,42 +139,162 @@ pub(super) async fn run_memories_startup_pipeline(
session: &Arc<Session>,
config: Arc<Config>,
) -> CodexResult<()> {
let Some(page) = state_db::list_threads_db(
session.services.state_db.as_deref(),
&config.codex_home,
PHASE_ONE_THREAD_SCAN_LIMIT,
None,
ThreadSortKey::UpdatedAt,
INTERACTIVE_SESSION_SOURCES,
None,
false,
)
.await
else {
let Some(state_db) = session.services.state_db.as_deref() else {
warn!("state db unavailable for memories startup pipeline; skipping");
return Ok(());
};
let phase_one = phase_one::run_phase_one(session, &page.items).await;
let allowed_sources = INTERACTIVE_SESSION_SOURCES
.iter()
.map(|value| match serde_json::to_value(value) {
Ok(Value::String(s)) => s,
Ok(other) => other.to_string(),
Err(_) => String::new(),
})
.collect::<Vec<_>>();
let claimed_candidates = match state_db
.claim_stage1_jobs_for_startup(
session.conversation_id,
PHASE_ONE_THREAD_SCAN_LIMIT,
super::MAX_ROLLOUTS_PER_STARTUP,
super::PHASE_ONE_MAX_ROLLOUT_AGE_DAYS,
allowed_sources.as_slice(),
super::PHASE_ONE_JOB_LEASE_SECONDS,
)
.await
{
Ok(claims) => claims,
Err(err) => {
warn!("state db claim_stage1_jobs_for_startup failed during memories startup: {err}");
Vec::new()
}
};
let claimed_count = claimed_candidates.len();
let mut succeeded_count = 0;
if claimed_count > 0 {
let turn_context = session.new_default_turn().await;
let stage_one_context = StageOneRequestContext::from_turn_context(
turn_context.as_ref(),
turn_context.resolve_turn_metadata_header().await,
);
succeeded_count = futures::stream::iter(claimed_candidates.into_iter())
.map(|claim| {
let session = Arc::clone(session);
let stage_one_context = stage_one_context.clone();
async move {
let thread = claim.thread;
let stage_one_output = match extract::extract_stage_one_output(
session.as_ref(),
&thread.rollout_path,
&stage_one_context,
)
.await
{
Ok(output) => output,
Err(reason) => {
if let Some(state_db) = session.services.state_db.as_deref() {
let _ = state_db
.mark_stage1_job_failed(
thread.id,
&claim.ownership_token,
reason,
super::PHASE_ONE_JOB_RETRY_DELAY_SECONDS,
)
.await;
}
return false;
}
};
let Some(state_db) = session.services.state_db.as_deref() else {
return false;
};
state_db
.mark_stage1_job_succeeded(
thread.id,
&claim.ownership_token,
thread.updated_at.timestamp(),
&stage_one_output.raw_memory,
&stage_one_output.summary,
)
.await
.unwrap_or(false)
}
})
.buffer_unordered(super::PHASE_ONE_CONCURRENCY_LIMIT)
.collect::<Vec<bool>>()
.await
.into_iter()
.filter(|ok| *ok)
.count();
}
info!(
"memory phase-1 candidate selection complete: {} claimed candidate(s) from {} indexed thread(s)",
phase_one.claimed_candidate_count,
page.items.len()
);
info!(
"memory phase-1 extraction complete: {} scope(s) touched",
phase_one.touched_scope_count
"memory stage-1 extraction complete: {} job(s) claimed, {} succeeded",
claimed_count, succeeded_count
);
let consolidation_scope_count = phase_two::run_phase_two(session, config).await;
let consolidation_scope_count = run_consolidation_dispatch(session, config).await;
info!(
"memory phase-2 consolidation dispatch complete: {} scope(s) scheduled",
"memory consolidation dispatch complete: {} scope(s) scheduled",
consolidation_scope_count
);
Ok(())
}
async fn run_consolidation_dispatch(session: &Arc<Session>, config: Arc<Config>) -> usize {
let scopes = list_consolidation_scopes(
session.as_ref(),
config.as_ref(),
super::MAX_ROLLOUTS_PER_STARTUP,
)
.await;
let consolidation_scope_count = scopes.len();
futures::stream::iter(scopes.into_iter())
.map(|scope| {
let session = Arc::clone(session);
let config = Arc::clone(&config);
async move {
dispatch::run_memory_consolidation_for_scope(session, config, scope).await;
}
})
.buffer_unordered(super::PHASE_TWO_CONCURRENCY_LIMIT)
.collect::<Vec<_>>()
.await;
consolidation_scope_count
}
async fn list_consolidation_scopes(
session: &Session,
config: &Config,
limit: usize,
) -> Vec<MemoryScopeTarget> {
if limit == 0 {
return Vec::new();
}
let Some(state_db) = session.services.state_db.as_deref() else {
return Vec::new();
};
let pending_scopes = match state_db.list_pending_scope_consolidations(limit).await {
Ok(scopes) => scopes,
Err(_) => return Vec::new(),
};
pending_scopes
.into_iter()
.filter_map(|scope| memory_scope_target_for_pending_scope(config, scope))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1,247 +0,0 @@
mod extract;
use codex_otel::OtelManager;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use extract::extract_stage_one_output;
use futures::StreamExt;
use tracing::warn;
use super::super::MAX_ROLLOUTS_PER_STARTUP;
use super::super::PHASE_ONE_CONCURRENCY_LIMIT;
use super::super::PHASE_ONE_JOB_LEASE_SECONDS;
use super::super::PHASE_ONE_JOB_RETRY_DELAY_SECONDS;
use super::super::PHASE_ONE_MAX_ROLLOUT_AGE_DAYS;
use super::super::selection::select_rollout_candidates_from_db;
use super::super::types::RolloutCandidate;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::memories::layout::memory_scope_key_for_cwd;
use crate::memories::scope::MEMORY_SCOPE_KEY_USER;
use crate::memories::scope::MEMORY_SCOPE_KIND_CWD;
use crate::memories::scope::MEMORY_SCOPE_KIND_USER;
use std::sync::Arc;
/// Result counters for startup phase-1 extraction.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct PhaseOneRunResult {
/// Number of rollout candidates that were successfully claimed.
pub(super) claimed_candidate_count: usize,
/// Number of scope refresh/enqueue operations performed.
pub(super) touched_scope_count: usize,
}
#[derive(Clone, Debug)]
pub(super) struct ClaimedStageOneCandidate {
pub(super) candidate: RolloutCandidate,
pub(super) ownership_token: String,
}
#[derive(Clone)]
struct StageOneRequestContext {
model_info: ModelInfo,
otel_manager: OtelManager,
reasoning_effort: Option<ReasoningEffortConfig>,
reasoning_summary: ReasoningSummaryConfig,
turn_metadata_header: Option<String>,
}
impl StageOneRequestContext {
fn from_turn_context(turn_context: &TurnContext, turn_metadata_header: Option<String>) -> Self {
Self {
model_info: turn_context.model_info.clone(),
otel_manager: turn_context.otel_manager.clone(),
reasoning_effort: turn_context.reasoning_effort,
reasoning_summary: turn_context.reasoning_summary,
turn_metadata_header,
}
}
}
/// Runs startup phase 1:
///
/// 1. Select rollout candidates from thread metadata.
/// 2. Claim stage-1 jobs per thread.
/// 3. Execute stage-1 extraction requests in parallel.
/// 4. Persist stage-1 outputs and enqueue consolidation jobs.
pub(super) async fn run_phase_one(
session: &Arc<Session>,
thread_items: &[codex_state::ThreadMetadata],
) -> PhaseOneRunResult {
let selection_candidates = select_rollout_candidates_from_db(
thread_items,
session.conversation_id,
super::PHASE_ONE_THREAD_SCAN_LIMIT,
PHASE_ONE_MAX_ROLLOUT_AGE_DAYS,
);
let claimed_candidates = claim_stage_one_candidates(
session.as_ref(),
selection_candidates,
MAX_ROLLOUTS_PER_STARTUP,
)
.await;
if claimed_candidates.is_empty() {
return PhaseOneRunResult {
claimed_candidate_count: 0,
touched_scope_count: 0,
};
}
let turn_context = session.new_default_turn().await;
let stage_one_context = StageOneRequestContext::from_turn_context(
turn_context.as_ref(),
turn_context.resolve_turn_metadata_header().await,
);
let touched_scope_count =
futures::stream::iter(claimed_candidates.iter().cloned())
.map(|claimed_candidate| {
let session = Arc::clone(session);
let stage_one_context = stage_one_context.clone();
async move {
process_memory_candidate(session, claimed_candidate, stage_one_context).await
}
})
.buffer_unordered(PHASE_ONE_CONCURRENCY_LIMIT)
.collect::<Vec<usize>>()
.await
.into_iter()
.sum::<usize>();
PhaseOneRunResult {
claimed_candidate_count: claimed_candidates.len(),
touched_scope_count,
}
}
async fn claim_stage_one_candidates(
session: &Session,
candidates: Vec<RolloutCandidate>,
max_claimed_candidates: usize,
) -> Vec<ClaimedStageOneCandidate> {
if max_claimed_candidates == 0 {
return Vec::new();
}
let Some(state_db) = session.services.state_db.as_deref() else {
return Vec::new();
};
let mut claimed = Vec::new();
for candidate in candidates {
if claimed.len() >= max_claimed_candidates {
break;
}
let claim = match state_db
.try_claim_stage1_job(
candidate.thread_id,
session.conversation_id,
candidate.source_updated_at,
PHASE_ONE_JOB_LEASE_SECONDS,
)
.await
{
Ok(claim) => claim,
Err(err) => {
warn!(
"state db try_claim_stage1_job failed for rollout {}: {err}",
candidate.rollout_path.display()
);
continue;
}
};
if let codex_state::Stage1JobClaimOutcome::Claimed { ownership_token } = claim {
claimed.push(ClaimedStageOneCandidate {
candidate,
ownership_token,
});
}
}
claimed
}
async fn process_memory_candidate(
session: Arc<Session>,
claimed_candidate: ClaimedStageOneCandidate,
stage_one_context: StageOneRequestContext,
) -> usize {
let candidate = claimed_candidate.candidate;
let stage_one_output =
match extract_stage_one_output(session.as_ref(), &candidate, &stage_one_context).await {
Ok(output) => output,
Err(reason) => {
if let Some(state_db) = session.services.state_db.as_deref() {
let _ = state_db
.mark_stage1_job_failed(
candidate.thread_id,
&claimed_candidate.ownership_token,
reason,
PHASE_ONE_JOB_RETRY_DELAY_SECONDS,
)
.await;
}
return 0;
}
};
let Some(state_db) = session.services.state_db.as_deref() else {
return 0;
};
if !state_db
.mark_stage1_job_succeeded(
candidate.thread_id,
&claimed_candidate.ownership_token,
candidate.source_updated_at,
&stage_one_output.raw_memory,
&stage_one_output.summary,
)
.await
.unwrap_or(false)
{
return 0;
}
let mut touched_scope_count = 0;
let cwd_scope_key = memory_scope_key_for_cwd(&candidate.cwd);
if let Err(err) = state_db
.enqueue_scope_consolidation(
MEMORY_SCOPE_KIND_CWD,
&cwd_scope_key,
candidate.source_updated_at,
)
.await
{
warn!(
"failed enqueueing scope consolidation for scope {}:{}: {err}",
MEMORY_SCOPE_KIND_CWD, cwd_scope_key
);
} else {
touched_scope_count += 1;
}
if let Err(err) = state_db
.enqueue_scope_consolidation(
MEMORY_SCOPE_KIND_USER,
MEMORY_SCOPE_KEY_USER,
candidate.source_updated_at,
)
.await
{
warn!(
"failed enqueueing scope consolidation for scope {}:{}: {err}",
MEMORY_SCOPE_KIND_USER, MEMORY_SCOPE_KEY_USER
);
} else {
touched_scope_count += 1;
}
touched_scope_count
}
@@ -1,60 +0,0 @@
mod dispatch;
mod watch;
use super::super::MAX_ROLLOUTS_PER_STARTUP;
use super::super::PHASE_TWO_CONCURRENCY_LIMIT;
use super::MemoryScopeTarget;
use super::memory_scope_target_for_pending_scope;
use crate::codex::Session;
use crate::config::Config;
use futures::StreamExt;
use std::sync::Arc;
/// Runs startup phase 2:
///
/// 1. Load scopes pending consolidation from the DB.
/// 2. Claim scope jobs.
/// 3. Spawn consolidation agents for owned scopes.
pub(super) async fn run_phase_two(session: &Arc<Session>, config: Arc<Config>) -> usize {
let scopes =
list_phase2_scopes(session.as_ref(), config.as_ref(), MAX_ROLLOUTS_PER_STARTUP).await;
let consolidation_scope_count = scopes.len();
futures::stream::iter(scopes.into_iter())
.map(|scope| {
let session = Arc::clone(session);
let config = Arc::clone(&config);
async move {
dispatch::run_memory_consolidation_for_scope(session, config, scope).await;
}
})
.buffer_unordered(PHASE_TWO_CONCURRENCY_LIMIT)
.collect::<Vec<_>>()
.await;
consolidation_scope_count
}
async fn list_phase2_scopes(
session: &Session,
config: &Config,
limit: usize,
) -> Vec<MemoryScopeTarget> {
if limit == 0 {
return Vec::new();
}
let Some(state_db) = session.services.state_db.as_deref() else {
return Vec::new();
};
let pending_scopes = match state_db.list_pending_scope_consolidations(limit).await {
Ok(scopes) => scopes,
Err(_) => return Vec::new(),
};
pending_scopes
.into_iter()
.filter_map(|scope| memory_scope_target_for_pending_scope(config, scope))
.collect()
}
@@ -7,10 +7,10 @@ use tracing::debug;
use tracing::info;
use tracing::warn;
use super::super::super::PHASE_TWO_JOB_HEARTBEAT_SECONDS;
use super::super::super::PHASE_TWO_JOB_LEASE_SECONDS;
use super::super::super::PHASE_TWO_JOB_RETRY_DELAY_SECONDS;
use super::super::MemoryScopeTarget;
use super::super::PHASE_TWO_JOB_HEARTBEAT_SECONDS;
use super::super::PHASE_TWO_JOB_LEASE_SECONDS;
use super::super::PHASE_TWO_JOB_RETRY_DELAY_SECONDS;
use super::MemoryScopeTarget;
pub(super) fn spawn_phase2_completion_task(
session: &Session,
-108
View File
@@ -1,15 +1,12 @@
use super::rollout::StageOneResponseItemKinds;
use super::rollout::StageOneRolloutFilter;
use super::rollout::serialize_filtered_rollout_response_items;
use super::selection::select_rollout_candidates_from_db;
use super::stage_one::parse_stage_one_output;
use super::storage::rebuild_memory_summary_from_memories;
use super::storage::sync_raw_memories_from_memories;
use super::storage::wipe_consolidation_outputs;
use crate::memories::PHASE_ONE_MAX_ROLLOUT_AGE_DAYS;
use crate::memories::layout::ensure_layout;
use crate::memories::layout::memory_root_for_cwd;
use crate::memories::layout::memory_scope_key_for_cwd;
use crate::memories::layout::memory_summary_file;
use crate::memories::layout::raw_memories_dir;
use chrono::TimeZone;
@@ -20,43 +17,9 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::CompactedItem;
use codex_protocol::protocol::RolloutItem;
use codex_state::Stage1Output;
use codex_state::ThreadMetadata;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use tempfile::tempdir;
fn thread_metadata(
thread_id: ThreadId,
path: PathBuf,
cwd: PathBuf,
title: &str,
updated_at_secs: i64,
) -> ThreadMetadata {
let updated_at = Utc
.timestamp_opt(updated_at_secs, 0)
.single()
.expect("timestamp");
ThreadMetadata {
id: thread_id,
rollout_path: path,
created_at: updated_at,
updated_at,
source: "cli".to_string(),
model_provider: "openai".to_string(),
cwd,
cli_version: "test".to_string(),
title: title.to_string(),
sandbox_policy: "read_only".to_string(),
approval_mode: "on_request".to_string(),
tokens_used: 0,
first_user_message: None,
archived_at: None,
git_branch: None,
git_sha: None,
git_origin_url: None,
}
}
#[test]
fn memory_root_varies_by_cwd() {
let dir = tempdir().expect("tempdir");
@@ -100,22 +63,6 @@ fn memory_root_encoding_avoids_component_collisions() {
assert!(!root_hash.display().to_string().contains("workspace"));
}
#[test]
fn memory_scope_key_uses_normalized_cwd() {
let dir = tempdir().expect("tempdir");
let workspace = dir.path().join("workspace");
std::fs::create_dir_all(&workspace).expect("mkdir workspace");
std::fs::create_dir_all(workspace.join("nested")).expect("mkdir nested");
let alias = workspace.join("nested").join("..");
let normalized = workspace
.canonicalize()
.expect("canonical workspace path should resolve");
let alias_key = memory_scope_key_for_cwd(&alias);
let normalized_key = memory_scope_key_for_cwd(&normalized);
assert_eq!(alias_key, normalized_key);
}
#[test]
fn parse_stage_one_output_accepts_fenced_json() {
let raw = "```json\n{\"rawMemory\":\"abc\",\"summary\":\"short\"}\n```";
@@ -224,61 +171,6 @@ fn serialize_filtered_rollout_response_items_filters_by_response_item_kind() {
assert!(matches!(parsed[0], ResponseItem::Message { .. }));
}
#[test]
fn select_rollout_candidates_filters_by_age_window() {
let dir = tempdir().expect("tempdir");
let cwd_a = dir.path().join("workspace-a");
let cwd_b = dir.path().join("workspace-b");
std::fs::create_dir_all(&cwd_a).expect("mkdir cwd a");
std::fs::create_dir_all(&cwd_b).expect("mkdir cwd b");
let now = Utc::now().timestamp();
let current_thread_id = ThreadId::default();
let recent_thread_id = ThreadId::default();
let old_thread_id = ThreadId::default();
let recent_two_thread_id = ThreadId::default();
let current = thread_metadata(
current_thread_id,
dir.path().join("current.jsonl"),
cwd_a.clone(),
"current",
now,
);
let recent = thread_metadata(
recent_thread_id,
dir.path().join("recent.jsonl"),
cwd_a,
"recent",
now - 10,
);
let old = thread_metadata(
old_thread_id,
dir.path().join("old.jsonl"),
cwd_b.clone(),
"old",
now - (PHASE_ONE_MAX_ROLLOUT_AGE_DAYS + 1) * 24 * 60 * 60,
);
let recent_two = thread_metadata(
recent_two_thread_id,
dir.path().join("recent-two.jsonl"),
cwd_b,
"recent-two",
now - 20,
);
let candidates = select_rollout_candidates_from_db(
&[current, recent, old, recent_two],
current_thread_id,
5,
PHASE_ONE_MAX_ROLLOUT_AGE_DAYS,
);
assert_eq!(candidates.len(), 2);
assert_eq!(candidates[0].thread_id, recent_thread_id);
assert_eq!(candidates[1].thread_id, recent_two_thread_id);
}
#[tokio::test]
async fn prune_and_rebuild_summary_keeps_latest_memories_only() {
let dir = tempdir().expect("tempdir");
-15
View File
@@ -1,19 +1,4 @@
use codex_protocol::ThreadId;
use serde::Deserialize;
use std::path::PathBuf;
/// A rollout selected for stage-1 memory extraction during startup.
#[derive(Debug, Clone)]
pub(super) struct RolloutCandidate {
/// Source thread identifier for this rollout.
pub(super) thread_id: ThreadId,
/// Absolute path to the rollout file to summarize.
pub(super) rollout_path: PathBuf,
/// Thread working directory used for per-project memory bucketing.
pub(super) cwd: PathBuf,
/// Thread update timestamp (unix seconds) used for stage-1 staleness checks.
pub(super) source_updated_at: i64,
}
/// Parsed stage-1 model output payload.
#[derive(Debug, Clone, Deserialize)]