@@ -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#"
200209INSERT 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
222232ON 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