diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index fc78f5b2f..3cd7fc809 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3033,6 +3033,12 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv Op::Compact => { handlers::compact(&sess, sub.id.clone()).await; } + Op::DropMemories => { + handlers::drop_memories(&sess, &config, sub.id.clone()).await; + } + Op::UpdateMemories => { + handlers::update_memories(&sess, &config, sub.id.clone()).await; + } Op::ThreadRollback { num_turns } => { handlers::thread_rollback(&sess, sub.id.clone(), num_turns).await; } @@ -3582,6 +3588,68 @@ mod handlers { .await; } + pub async fn drop_memories(sess: &Arc, config: &Arc, sub_id: String) { + let mut errors = Vec::new(); + + if let Some(state_db) = sess.services.state_db.as_deref() { + if let Err(err) = state_db.clear_memory_data().await { + errors.push(format!("failed clearing memory rows from state db: {err}")); + } + } else { + errors.push("state db unavailable; memory rows were not cleared".to_string()); + } + + let memory_root = crate::memories::memory_root(&config.codex_home); + if let Err(err) = tokio::fs::remove_dir_all(&memory_root).await + && err.kind() != std::io::ErrorKind::NotFound + { + errors.push(format!( + "failed removing memory directory {}: {err}", + memory_root.display() + )); + } + + if errors.is_empty() { + sess.send_event_raw(Event { + id: sub_id, + msg: EventMsg::Warning(WarningEvent { + message: format!( + "Dropped memories at {} and cleared memory rows from state db.", + memory_root.display() + ), + }), + }) + .await; + return; + } + + sess.send_event_raw(Event { + id: sub_id, + msg: EventMsg::Error(ErrorEvent { + message: format!("Memory drop completed with errors: {}", errors.join("; ")), + codex_error_info: Some(CodexErrorInfo::Other), + }), + }) + .await; + } + + pub async fn update_memories(sess: &Arc, config: &Arc, sub_id: String) { + let session_source = { + let state = sess.state.lock().await; + state.session_configuration.session_source.clone() + }; + + crate::memories::start_memories_startup_task(sess, Arc::clone(config), &session_source); + + sess.send_event_raw(Event { + id: sub_id.clone(), + msg: EventMsg::Warning(WarningEvent { + message: "Memory update triggered.".to_string(), + }), + }) + .await; + } + pub async fn thread_rollback(sess: &Arc, sub_id: String, num_turns: u32) { if num_turns == 0 { sess.send_event_raw(Event { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 5ff6407f1..40e981b93 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -290,6 +290,12 @@ pub enum Op { /// to generate a summary which will be returned as an AgentMessage event. Compact, + /// Drop all persisted memory artifacts and memory-tracking DB rows. + DropMemories, + + /// Trigger a single pass of the startup memory pipeline. + UpdateMemories, + /// Set a user-facing thread name in the persisted rollout metadata. /// This is a local-only operation handled by codex-core; it does not /// involve the model. diff --git a/codex-rs/state/src/runtime/memories.rs b/codex-rs/state/src/runtime/memories.rs index 5fe28ab11..162543bda 100644 --- a/codex-rs/state/src/runtime/memories.rs +++ b/codex-rs/state/src/runtime/memories.rs @@ -18,6 +18,32 @@ const MEMORY_CONSOLIDATION_JOB_KEY: &str = "global"; const DEFAULT_RETRY_REMAINING: i64 = 3; impl StateRuntime { + pub async fn clear_memory_data(&self) -> anyhow::Result<()> { + let mut tx = self.pool.begin().await?; + + sqlx::query( + r#" +DELETE FROM stage1_outputs + "#, + ) + .execute(&mut *tx) + .await?; + + sqlx::query( + r#" +DELETE FROM jobs +WHERE kind = ? OR kind = ? + "#, + ) + .bind(JOB_KIND_MEMORY_STAGE1) + .bind(JOB_KIND_MEMORY_CONSOLIDATE_GLOBAL) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + Ok(()) + } + pub async fn claim_stage1_jobs_for_startup( &self, current_thread_id: ThreadId, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index d9f48a2a2..0ae056f83 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -3398,6 +3398,12 @@ impl ChatWidget { SlashCommand::Clean => { self.clean_background_terminals(); } + SlashCommand::MemoryDrop => { + self.submit_op(Op::DropMemories); + } + SlashCommand::MemoryUpdate => { + self.submit_op(Op::UpdateMemories); + } SlashCommand::Mcp => { self.add_mcp_output(); } diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 6c6e9ccde..a126c5f98 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -3380,6 +3380,24 @@ async fn slash_clean_submits_background_terminal_cleanup() { ); } +#[tokio::test] +async fn slash_memory_drop_submits_drop_memories_op() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + + chat.dispatch_command(SlashCommand::MemoryDrop); + + assert_matches!(op_rx.try_recv(), Ok(Op::DropMemories)); +} + +#[tokio::test] +async fn slash_memory_update_submits_update_memories_op() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + + chat.dispatch_command(SlashCommand::MemoryUpdate); + + assert_matches!(op_rx.try_recv(), Ok(Op::UpdateMemories)); +} + #[tokio::test] async fn slash_resume_opens_picker() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index e48ac74e7..a1699c37b 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -46,6 +46,11 @@ pub enum SlashCommand { Clean, Personality, TestApproval, + // Debugging commands. + #[strum(serialize = "debug-m-drop")] + MemoryDrop, + #[strum(serialize = "debug-m-update")] + MemoryUpdate, } impl SlashCommand { @@ -70,6 +75,8 @@ impl SlashCommand { SlashCommand::Statusline => "configure which items appear in the status line", SlashCommand::Ps => "list background terminals", SlashCommand::Clean => "stop all background terminals", + SlashCommand::MemoryDrop => "DO NOT USE", + SlashCommand::MemoryUpdate => "DO NOT USE", SlashCommand::Model => "choose what model and reasoning effort to use", SlashCommand::Personality => "choose a communication style for Codex", SlashCommand::Plan => "switch to Plan mode", @@ -118,7 +125,9 @@ impl SlashCommand { | SlashCommand::Experimental | SlashCommand::Review | SlashCommand::Plan - | SlashCommand::Logout => false, + | SlashCommand::Logout + | SlashCommand::MemoryDrop + | SlashCommand::MemoryUpdate => false, SlashCommand::Diff | SlashCommand::Rename | SlashCommand::Mention