mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add app-server thread/delete API (#25018)
## Why Clients can archive and unarchive threads today, but there is no app-server API for permanently removing a thread. Deletion also needs to cover the full session tree: deleting a main thread should remove spawned subagent threads and the related local metadata instead of leaving orphaned rollout files, goals, or subagent state behind. ## What - Adds the v2 `thread/delete` request and `thread/deleted` notification, with the response shape kept consistent with `thread/archive`. - Implements local hard delete for active and archived rollout files. - Deletes the requested thread's state DB row as the commit point, then best-effort cleans associated state including spawned descendants, goals, spawn edges, logs, dynamic tools, and agent job assignments. - Updates app-server API docs and generated protocol schema/TypeScript fixtures.
This commit is contained in:
@@ -73,6 +73,7 @@ pub use session_index::append_thread_name;
|
||||
pub use session_index::find_thread_meta_by_name_str;
|
||||
pub use session_index::find_thread_name_by_id;
|
||||
pub use session_index::find_thread_names_by_ids;
|
||||
pub use session_index::remove_thread_name_entries;
|
||||
pub use state_db::StateDbHandle;
|
||||
pub use state_db::sqlite_telemetry_recorder;
|
||||
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::ErrorKind;
|
||||
use std::io::Read;
|
||||
use std::io::Seek;
|
||||
use std::io::SeekFrom;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SessionMetaLine;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
const SESSION_INDEX_FILE: &str = "session_index.jsonl";
|
||||
const READ_CHUNK_SIZE: usize = 8192;
|
||||
static SESSION_INDEX_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SessionIndexEntry {
|
||||
@@ -25,7 +29,7 @@ pub struct SessionIndexEntry {
|
||||
}
|
||||
|
||||
/// Append a thread name update to the session index.
|
||||
/// The index is append-only; the most recent entry wins when resolving names or ids.
|
||||
/// Name updates are append-only; the most recent entry wins when resolving names or ids.
|
||||
pub async fn append_thread_name(
|
||||
codex_home: &Path,
|
||||
thread_id: ThreadId,
|
||||
@@ -46,24 +50,60 @@ pub async fn append_thread_name(
|
||||
}
|
||||
|
||||
/// Append a raw session index entry to `session_index.jsonl`.
|
||||
/// The file is append-only; consumers scan from the end to find the newest match.
|
||||
/// Consumers scan from the end to find the newest match.
|
||||
pub async fn append_session_index_entry(
|
||||
codex_home: &Path,
|
||||
entry: &SessionIndexEntry,
|
||||
) -> std::io::Result<()> {
|
||||
let _guard = SESSION_INDEX_LOCK
|
||||
.lock()
|
||||
.map_err(|err| std::io::Error::other(err.to_string()))?;
|
||||
let path = session_index_path(codex_home);
|
||||
let mut file = tokio::fs::OpenOptions::new()
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.await?;
|
||||
.open(&path)?;
|
||||
let mut line = serde_json::to_string(entry).map_err(std::io::Error::other)?;
|
||||
line.push('\n');
|
||||
file.write_all(line.as_bytes()).await?;
|
||||
file.flush().await?;
|
||||
file.write_all(line.as_bytes())?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove all recorded names for a thread from the session index.
|
||||
pub async fn remove_thread_name_entries(
|
||||
codex_home: &Path,
|
||||
thread_id: ThreadId,
|
||||
) -> std::io::Result<()> {
|
||||
let _guard = SESSION_INDEX_LOCK
|
||||
.lock()
|
||||
.map_err(|err| std::io::Error::other(err.to_string()))?;
|
||||
let path = session_index_path(codex_home);
|
||||
let contents = match std::fs::read_to_string(&path) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
let mut removed = false;
|
||||
let mut remaining = String::with_capacity(contents.len());
|
||||
for line in contents.lines() {
|
||||
let should_remove = serde_json::from_str::<SessionIndexEntry>(line.trim())
|
||||
.is_ok_and(|entry| entry.id == thread_id);
|
||||
if should_remove {
|
||||
removed = true;
|
||||
} else {
|
||||
remaining.push_str(line);
|
||||
remaining.push('\n');
|
||||
}
|
||||
}
|
||||
if !removed {
|
||||
return Ok(());
|
||||
}
|
||||
let temp_path = path.with_extension("jsonl.tmp");
|
||||
std::fs::write(&temp_path, remaining)?;
|
||||
std::fs::rename(temp_path, path)
|
||||
}
|
||||
|
||||
/// Find the latest thread name for a thread id, if any.
|
||||
pub async fn find_thread_name_by_id(
|
||||
codex_home: &Path,
|
||||
|
||||
Reference in New Issue
Block a user