Skip to content

Commit bbd2373

Browse files
authored
feat: gen memories config (#12999)
1 parent a63d8bd commit bbd2373

File tree

6 files changed

+98
-7
lines changed

6 files changed

+98
-7
lines changed

codex-rs/core/config.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,10 @@
613613
"additionalProperties": false,
614614
"description": "Memories settings loaded from config.toml.",
615615
"properties": {
616+
"generate_memories": {
617+
"description": "When `false`, newly created threads are stored with `memory_mode = \"disabled\"` in the state DB.",
618+
"type": "boolean"
619+
},
616620
"max_raw_memories_for_global": {
617621
"description": "Maximum number of recent raw memories retained for global consolidation.",
618622
"format": "uint",

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2490,6 +2490,7 @@ persistence = "none"
24902490

24912491
let memories = r#"
24922492
[memories]
2493+
generate_memories = false
24932494
use_memories = false
24942495
max_raw_memories_for_global = 512
24952496
max_unused_days = 21
@@ -2503,6 +2504,7 @@ phase_2_model = "gpt-5"
25032504
toml::from_str::<ConfigToml>(memories).expect("TOML deserialization should succeed");
25042505
assert_eq!(
25052506
Some(MemoriesToml {
2507+
generate_memories: Some(false),
25062508
use_memories: Some(false),
25072509
max_raw_memories_for_global: Some(512),
25082510
max_unused_days: Some(21),
@@ -2524,6 +2526,7 @@ phase_2_model = "gpt-5"
25242526
assert_eq!(
25252527
config.memories,
25262528
MemoriesConfig {
2529+
generate_memories: false,
25272530
use_memories: false,
25282531
max_raw_memories_for_global: 512,
25292532
max_unused_days: 21,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ pub struct FeedbackConfigToml {
371371
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)]
372372
#[schemars(deny_unknown_fields)]
373373
pub struct MemoriesToml {
374+
/// When `false`, newly created threads are stored with `memory_mode = "disabled"` in the state DB.
375+
pub generate_memories: Option<bool>,
374376
/// When `false`, skip injecting memory usage instructions into developer prompts.
375377
pub use_memories: Option<bool>,
376378
/// Maximum number of recent raw memories retained for global consolidation.
@@ -392,6 +394,7 @@ pub struct MemoriesToml {
392394
/// Effective memories settings after defaults are applied.
393395
#[derive(Debug, Clone, PartialEq, Eq)]
394396
pub struct MemoriesConfig {
397+
pub generate_memories: bool,
395398
pub use_memories: bool,
396399
pub max_raw_memories_for_global: usize,
397400
pub max_unused_days: i64,
@@ -405,6 +408,7 @@ pub struct MemoriesConfig {
405408
impl Default for MemoriesConfig {
406409
fn default() -> Self {
407410
Self {
411+
generate_memories: true,
408412
use_memories: true,
409413
max_raw_memories_for_global: DEFAULT_MEMORIES_MAX_RAW_MEMORIES_FOR_GLOBAL,
410414
max_unused_days: DEFAULT_MEMORIES_MAX_UNUSED_DAYS,
@@ -421,6 +425,7 @@ impl From<MemoriesToml> for MemoriesConfig {
421425
fn from(toml: MemoriesToml) -> Self {
422426
let defaults = Self::default();
423427
Self {
428+
generate_memories: toml.generate_memories.unwrap_or(defaults.generate_memories),
424429
use_memories: toml.use_memories.unwrap_or(defaults.use_memories),
425430
max_raw_memories_for_global: toml
426431
.max_raw_memories_for_global

codex-rs/core/src/rollout/recorder.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ impl RolloutRecorder {
460460
state_db_ctx.clone(),
461461
state_builder,
462462
config.model_provider_id.clone(),
463+
config.memories.generate_memories,
463464
));
464465

465466
Ok(Self {
@@ -711,6 +712,7 @@ async fn rollout_writer(
711712
state_db_ctx: Option<StateDbHandle>,
712713
mut state_builder: Option<ThreadMetadataBuilder>,
713714
default_provider: String,
715+
generate_memories: bool,
714716
) -> std::io::Result<()> {
715717
let mut writer = file.map(|file| JsonlWriter { file });
716718
let mut buffered_items = Vec::<RolloutItem>::new();
@@ -731,6 +733,7 @@ async fn rollout_writer(
731733
state_db_ctx.as_deref(),
732734
&mut state_builder,
733735
default_provider.as_str(),
736+
generate_memories,
734737
)
735738
.await?;
736739
}
@@ -784,6 +787,7 @@ async fn rollout_writer(
784787
state_db_ctx.as_deref(),
785788
&mut state_builder,
786789
default_provider.as_str(),
790+
generate_memories,
787791
)
788792
.await?;
789793
}
@@ -831,6 +835,7 @@ async fn rollout_writer(
831835
Ok(())
832836
}
833837

838+
#[allow(clippy::too_many_arguments)]
834839
async fn write_session_meta(
835840
mut writer: Option<&mut JsonlWriter>,
836841
session_meta: SessionMeta,
@@ -839,6 +844,7 @@ async fn write_session_meta(
839844
state_db_ctx: Option<&StateRuntime>,
840845
state_builder: &mut Option<ThreadMetadataBuilder>,
841846
default_provider: &str,
847+
generate_memories: bool,
842848
) -> std::io::Result<()> {
843849
let git_info = collect_git_info(cwd).await;
844850
let session_meta_line = SessionMetaLine {
@@ -860,6 +866,7 @@ async fn write_session_meta(
860866
state_builder.as_ref(),
861867
std::slice::from_ref(&rollout_item),
862868
None,
869+
(!generate_memories).then_some("disabled"),
863870
)
864871
.await;
865872
Ok(())
@@ -888,6 +895,7 @@ async fn write_and_reconcile_items(
888895
state_builder.as_ref(),
889896
items,
890897
"rollout_writer",
898+
None,
891899
)
892900
.await;
893901
Ok(())

codex-rs/core/src/state_db.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ pub async fn reconcile_rollout(
345345
builder: Option<&ThreadMetadataBuilder>,
346346
items: &[RolloutItem],
347347
archived_only: Option<bool>,
348+
new_thread_memory_mode: Option<&str>,
348349
) {
349350
let Some(ctx) = context else {
350351
return;
@@ -357,6 +358,7 @@ pub async fn reconcile_rollout(
357358
builder,
358359
items,
359360
"reconcile_rollout",
361+
new_thread_memory_mode,
360362
)
361363
.await;
362364
return;
@@ -467,6 +469,7 @@ pub async fn read_repair_rollout_path(
467469
None,
468470
&[],
469471
archived_only,
472+
None,
470473
)
471474
.await;
472475
}
@@ -479,6 +482,7 @@ pub async fn apply_rollout_items(
479482
builder: Option<&ThreadMetadataBuilder>,
480483
items: &[RolloutItem],
481484
stage: &str,
485+
new_thread_memory_mode: Option<&str>,
482486
) {
483487
let Some(ctx) = context else {
484488
return;
@@ -499,7 +503,10 @@ pub async fn apply_rollout_items(
499503
};
500504
builder.rollout_path = rollout_path.to_path_buf();
501505
builder.cwd = normalize_cwd_for_state_db(&builder.cwd);
502-
if let Err(err) = ctx.apply_rollout_items(&builder, items, None).await {
506+
if let Err(err) = ctx
507+
.apply_rollout_items(&builder, items, None, new_thread_memory_mode)
508+
.await
509+
{
503510
warn!(
504511
"state db apply_rollout_items failed during {stage} for {}: {err}",
505512
rollout_path.display()

codex-rs/state/src/runtime/threads.rs

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,15 @@ FROM threads
195195

196196
/// Insert or replace thread metadata directly.
197197
pub async fn upsert_thread(&self, metadata: &crate::ThreadMetadata) -> anyhow::Result<()> {
198+
self.upsert_thread_with_creation_memory_mode(metadata, None)
199+
.await
200+
}
201+
202+
async fn upsert_thread_with_creation_memory_mode(
203+
&self,
204+
metadata: &crate::ThreadMetadata,
205+
creation_memory_mode: Option<&str>,
206+
) -> anyhow::Result<()> {
198207
sqlx::query(
199208
r#"
200209
INSERT INTO threads (
@@ -217,8 +226,9 @@ INSERT INTO threads (
217226
archived_at,
218227
git_sha,
219228
git_branch,
220-
git_origin_url
221-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
229+
git_origin_url,
230+
memory_mode
231+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
222232
ON CONFLICT(id) DO UPDATE SET
223233
rollout_path = excluded.rollout_path,
224234
created_at = excluded.created_at,
@@ -261,6 +271,7 @@ ON CONFLICT(id) DO UPDATE SET
261271
.bind(metadata.git_sha.as_deref())
262272
.bind(metadata.git_branch.as_deref())
263273
.bind(metadata.git_origin_url.as_deref())
274+
.bind(creation_memory_mode.unwrap_or("enabled"))
264275
.execute(self.pool.as_ref())
265276
.await?;
266277
Ok(())
@@ -316,13 +327,14 @@ ON CONFLICT(thread_id, position) DO NOTHING
316327
builder: &ThreadMetadataBuilder,
317328
items: &[RolloutItem],
318329
otel: Option<&OtelManager>,
330+
new_thread_memory_mode: Option<&str>,
319331
) -> anyhow::Result<()> {
320332
if items.is_empty() {
321333
return Ok(());
322334
}
323-
let mut metadata = self
324-
.get_thread(builder.id)
325-
.await?
335+
let existing_metadata = self.get_thread(builder.id).await?;
336+
let mut metadata = existing_metadata
337+
.clone()
326338
.unwrap_or_else(|| builder.build(&self.default_provider));
327339
metadata.rollout_path = builder.rollout_path.clone();
328340
for item in items {
@@ -333,7 +345,13 @@ ON CONFLICT(thread_id, position) DO NOTHING
333345
}
334346
// Keep the thread upsert before dynamic tools to satisfy the foreign key constraint:
335347
// thread_dynamic_tools.thread_id -> threads.id.
336-
if let Err(err) = self.upsert_thread(&metadata).await {
348+
let upsert_result = if existing_metadata.is_none() {
349+
self.upsert_thread_with_creation_memory_mode(&metadata, new_thread_memory_mode)
350+
.await
351+
} else {
352+
self.upsert_thread(&metadata).await
353+
};
354+
if let Err(err) = upsert_result {
337355
if let Some(otel) = otel {
338356
otel.counter(DB_ERROR_METRIC, 1, &[("stage", "apply_rollout_items")]);
339357
}
@@ -494,3 +512,49 @@ pub(super) fn push_thread_order_and_limit(
494512
builder.push(" LIMIT ");
495513
builder.push_bind(limit as i64);
496514
}
515+
516+
#[cfg(test)]
517+
mod tests {
518+
use super::*;
519+
use crate::runtime::test_support::test_thread_metadata;
520+
use crate::runtime::test_support::unique_temp_dir;
521+
use pretty_assertions::assert_eq;
522+
523+
#[tokio::test]
524+
async fn upsert_thread_keeps_creation_memory_mode_for_existing_rows() {
525+
let codex_home = unique_temp_dir();
526+
let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string(), None)
527+
.await
528+
.expect("state db should initialize");
529+
let thread_id =
530+
ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread id");
531+
let mut metadata = test_thread_metadata(&codex_home, thread_id, codex_home.clone());
532+
533+
runtime
534+
.upsert_thread_with_creation_memory_mode(&metadata, Some("disabled"))
535+
.await
536+
.expect("initial insert should succeed");
537+
538+
let memory_mode: String =
539+
sqlx::query_scalar("SELECT memory_mode FROM threads WHERE id = ?")
540+
.bind(thread_id.to_string())
541+
.fetch_one(runtime.pool.as_ref())
542+
.await
543+
.expect("memory mode should be readable");
544+
assert_eq!(memory_mode, "disabled");
545+
546+
metadata.title = "updated title".to_string();
547+
runtime
548+
.upsert_thread(&metadata)
549+
.await
550+
.expect("upsert should succeed");
551+
552+
let memory_mode: String =
553+
sqlx::query_scalar("SELECT memory_mode FROM threads WHERE id = ?")
554+
.bind(thread_id.to_string())
555+
.fetch_one(runtime.pool.as_ref())
556+
.await
557+
.expect("memory mode should remain readable");
558+
assert_eq!(memory_mode, "disabled");
559+
}
560+
}

0 commit comments

Comments
 (0)