Skip to content

Commit c528f32

Browse files
authored
feat: use memory usage for selection (openai#12909)
1 parent 1503a8d commit c528f32

8 files changed

Lines changed: 329 additions & 16 deletions

File tree

codex-rs/core/config.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,11 @@
630630
"minimum": 0.0,
631631
"type": "integer"
632632
},
633+
"max_unused_days": {
634+
"description": "Maximum number of days since a memory was last used before it becomes ineligible for phase 2 selection.",
635+
"format": "int64",
636+
"type": "integer"
637+
},
633638
"min_rollout_idle_hours": {
634639
"description": "Minimum idle time between last thread activity and memory creation (hours). > 12h recommended.",
635640
"format": "int64",

codex-rs/core/src/config/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,6 +2467,7 @@ persistence = "none"
24672467
let memories = r#"
24682468
[memories]
24692469
max_raw_memories_for_global = 512
2470+
max_unused_days = 21
24702471
max_rollout_age_days = 42
24712472
max_rollouts_per_startup = 9
24722473
min_rollout_idle_hours = 24
@@ -2478,6 +2479,7 @@ phase_2_model = "gpt-5"
24782479
assert_eq!(
24792480
Some(MemoriesToml {
24802481
max_raw_memories_for_global: Some(512),
2482+
max_unused_days: Some(21),
24812483
max_rollout_age_days: Some(42),
24822484
max_rollouts_per_startup: Some(9),
24832485
min_rollout_idle_hours: Some(24),
@@ -2497,6 +2499,7 @@ phase_2_model = "gpt-5"
24972499
config.memories,
24982500
MemoriesConfig {
24992501
max_raw_memories_for_global: 512,
2502+
max_unused_days: 21,
25002503
max_rollout_age_days: 42,
25012504
max_rollouts_per_startup: 9,
25022505
min_rollout_idle_hours: 24,

codex-rs/core/src/config/types.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub const DEFAULT_MEMORIES_MAX_ROLLOUTS_PER_STARTUP: usize = 16;
2727
pub const DEFAULT_MEMORIES_MAX_ROLLOUT_AGE_DAYS: i64 = 30;
2828
pub const DEFAULT_MEMORIES_MIN_ROLLOUT_IDLE_HOURS: i64 = 6;
2929
pub const DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL: usize = 1_024;
30+
pub const DEFAULT_MEMORIES_MAX_UNUSED_DAYS: i64 = 30;
3031

3132
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
3233
#[serde(rename_all = "kebab-case")]
@@ -363,6 +364,8 @@ pub struct FeedbackConfigToml {
363364
pub struct MemoriesToml {
364365
/// Maximum number of recent raw memories retained for global consolidation.
365366
pub max_raw_memories_for_global: Option<usize>,
367+
/// Maximum number of days since a memory was last used before it becomes ineligible for phase 2 selection.
368+
pub max_unused_days: Option<i64>,
366369
/// Maximum age of the threads used for memories.
367370
pub max_rollout_age_days: Option<i64>,
368371
/// Maximum number of rollout candidates processed per pass.
@@ -379,6 +382,7 @@ pub struct MemoriesToml {
379382
#[derive(Debug, Clone, PartialEq, Eq)]
380383
pub struct MemoriesConfig {
381384
pub max_raw_memories_for_global: usize,
385+
pub max_unused_days: i64,
382386
pub max_rollout_age_days: i64,
383387
pub max_rollouts_per_startup: usize,
384388
pub min_rollout_idle_hours: i64,
@@ -390,6 +394,7 @@ impl Default for MemoriesConfig {
390394
fn default() -> Self {
391395
Self {
392396
max_raw_memories_for_global: DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL,
397+
max_unused_days: DEFAULT_MEMORIES_MAX_UNUSED_DAYS,
393398
max_rollout_age_days: DEFAULT_MEMORIES_MAX_ROLLOUT_AGE_DAYS,
394399
max_rollouts_per_startup: DEFAULT_MEMORIES_MAX_ROLLOUTS_PER_STARTUP,
395400
min_rollout_idle_hours: DEFAULT_MEMORIES_MIN_ROLLOUT_IDLE_HOURS,
@@ -407,6 +412,10 @@ impl From<MemoriesToml> for MemoriesConfig {
407412
.max_raw_memories_for_global
408413
.unwrap_or(defaults.max_raw_memories_for_global)
409414
.min(4096),
415+
max_unused_days: toml
416+
.max_unused_days
417+
.unwrap_or(defaults.max_unused_days)
418+
.clamp(0, 365),
410419
max_rollout_age_days: toml
411420
.max_rollout_age_days
412421
.unwrap_or(defaults.max_rollout_age_days)

codex-rs/core/src/memories/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,14 @@ Phase 2 consolidates the latest stage-1 outputs into the filesystem memory artif
5959
What it does:
6060

6161
- claims a single global phase-2 job (so only one consolidation runs at a time)
62-
- loads a bounded set of the most recent stage-1 outputs from the state DB (the per-rollout memories produced by Phase 1, used as the consolidation input set)
62+
- loads a bounded set of stage-1 outputs from the state DB using phase-2
63+
selection rules:
64+
- ignores memories whose `last_usage` falls outside the configured
65+
`max_unused_days` window
66+
- for memories with no `last_usage`, falls back to `generated_at` so fresh
67+
never-used memories can still be selected
68+
- ranks eligible memories by `usage_count` first, then by the most recent
69+
`last_usage` / `generated_at`
6370
- computes a completion watermark from the claimed watermark + newest input timestamps
6471
- syncs local memory artifacts under the memories root:
6572
- `raw_memories.md` (merged raw memories, latest first)

codex-rs/core/src/memories/phase2.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ pub(super) async fn run(session: &Arc<Session>, config: Arc<Config>) {
5353
};
5454
let root = memory_root(&config.codex_home);
5555
let max_raw_memories = config.memories.max_raw_memories_for_global;
56+
let max_unused_days = config.memories.max_unused_days;
5657

5758
// 1. Claim the job.
5859
let claim = match job::claim(session, db).await {
@@ -76,7 +77,10 @@ pub(super) async fn run(session: &Arc<Session>, config: Arc<Config>) {
7677
};
7778

7879
// 3. Query the memories
79-
let selection = match db.get_phase2_input_selection(max_raw_memories).await {
80+
let selection = match db
81+
.get_phase2_input_selection(max_raw_memories, max_unused_days)
82+
.await
83+
{
8084
Ok(selection) => selection,
8185
Err(err) => {
8286
tracing::error!("failed to list stage1 outputs from global: {}", err);

codex-rs/core/src/memories/tests.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ mod phase2 {
559559
#[tokio::test]
560560
async fn dispatch_reclaims_stale_global_lock_and_starts_consolidation() {
561561
let harness = DispatchHarness::new().await;
562-
harness.seed_stage1_output(100).await;
562+
harness.seed_stage1_output(Utc::now().timestamp()).await;
563563

564564
let stale_claim = harness
565565
.state_db
@@ -573,12 +573,18 @@ mod phase2 {
573573

574574
phase2::run(&harness.session, Arc::clone(&harness.config)).await;
575575

576-
let running_claim = harness
576+
let post_dispatch_claim = harness
577577
.state_db
578578
.try_claim_global_phase2_job(ThreadId::new(), 3_600)
579579
.await
580-
.expect("claim while running");
581-
pretty_assertions::assert_eq!(running_claim, Phase2JobClaimOutcome::SkippedRunning);
580+
.expect("claim after stale lock dispatch");
581+
assert!(
582+
matches!(
583+
post_dispatch_claim,
584+
Phase2JobClaimOutcome::SkippedRunning | Phase2JobClaimOutcome::SkippedNotDirty
585+
),
586+
"stale-lock dispatch should either keep the reclaimed job running or finish it before re-claim"
587+
);
582588

583589
let user_input_ops = harness.user_input_ops_count();
584590
pretty_assertions::assert_eq!(user_input_ops, 1);

codex-rs/core/tests/suite/memories.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ async fn wait_for_phase2_success(
248248
) -> Result<()> {
249249
let deadline = Instant::now() + Duration::from_secs(10);
250250
loop {
251-
let selection = db.get_phase2_input_selection(1).await?;
251+
let selection = db.get_phase2_input_selection(1, 30).await?;
252252
if selection.selected.len() == 1
253253
&& selection.selected[0].thread_id == expected_thread_id
254254
&& selection.retained_thread_ids == vec![expected_thread_id]

0 commit comments

Comments
 (0)