mirror of
https://github.com/openai/codex.git
synced 2026-11-01 19:33:35 +00:00
Add thread section management APIs (#36380)
## What changed - Add `threadSection/create`, `threadSection/update`, and `threadSection/delete` app-server methods, with generated protocol schemas and TypeScript bindings. - Persist custom sections in SQLite with stable UUIDv7 identities, trim and validate display names, and prevent renaming or deleting the built-in pinned section. - Delete sections transactionally and return their active and archived threads to the unsectioned list. - Serialize section mutations with section listing so concurrent requests observe ordered updates. ## Testing - Cover section persistence across restarts, ordered renames, member cleanup on deletion, invalid requests, unsupported stores, and thread-store/state-runtime behavior. GitOrigin-RevId: dc9a4be283c21dbc93ab1829eafc4825d8df6575
This commit is contained in:
committed by
copyberry
parent
d62353a312
commit
c42ea41ee0
@@ -10,6 +10,7 @@ mod live_thread;
|
||||
mod local;
|
||||
mod store;
|
||||
mod thread_metadata_sync;
|
||||
mod thread_sections;
|
||||
mod types;
|
||||
|
||||
pub use error::ThreadStoreError;
|
||||
@@ -22,6 +23,12 @@ pub use local::LocalThreadStore;
|
||||
pub use local::LocalThreadStoreConfig;
|
||||
pub use store::ThreadStore;
|
||||
pub use store::ThreadStoreFuture;
|
||||
pub use thread_sections::CreateThreadSectionParams;
|
||||
pub use thread_sections::DeleteThreadSectionParams;
|
||||
pub use thread_sections::ListThreadSectionsParams;
|
||||
pub use thread_sections::RenameThreadSectionParams;
|
||||
pub use thread_sections::StoredThreadSection;
|
||||
pub use thread_sections::StoredThreadSectionsPage;
|
||||
pub use types::AppendThreadItemsParams;
|
||||
pub use types::ArchiveThreadParams;
|
||||
pub use types::ArchiveThreadsParams;
|
||||
|
||||
@@ -14,6 +14,7 @@ mod rollout_lineage;
|
||||
mod search_threads;
|
||||
mod thread_history;
|
||||
mod thread_history_materialization;
|
||||
mod thread_sections;
|
||||
mod unarchive_thread;
|
||||
mod update_thread_metadata;
|
||||
mod writer_lock;
|
||||
@@ -41,10 +42,13 @@ use crate::AppendThreadItemsParams;
|
||||
use crate::ArchiveThreadParams;
|
||||
use crate::ArchiveThreadsParams;
|
||||
use crate::CreateThreadParams;
|
||||
use crate::CreateThreadSectionParams;
|
||||
use crate::DeleteThreadParams;
|
||||
use crate::DeleteThreadSectionParams;
|
||||
use crate::DeleteThreadsParams;
|
||||
use crate::ItemPage;
|
||||
use crate::ListItemsParams;
|
||||
use crate::ListThreadSectionsParams;
|
||||
use crate::ListThreadsParams;
|
||||
use crate::ListTurnsParams;
|
||||
use crate::LoadThreadHistoryParams;
|
||||
@@ -53,12 +57,15 @@ use crate::PrepareForkParams;
|
||||
use crate::PreparedFork;
|
||||
use crate::ReadThreadByRolloutPathParams;
|
||||
use crate::ReadThreadParams;
|
||||
use crate::RenameThreadSectionParams;
|
||||
use crate::ResumeThreadParams;
|
||||
use crate::SearchThreadOccurrencesParams;
|
||||
use crate::SearchThreadsParams;
|
||||
use crate::StoredModelContext;
|
||||
use crate::StoredThread;
|
||||
use crate::StoredThreadHistory;
|
||||
use crate::StoredThreadSection;
|
||||
use crate::StoredThreadSectionsPage;
|
||||
use crate::ThreadOccurrenceSearchPage;
|
||||
use crate::ThreadPage;
|
||||
use crate::ThreadSearchPage;
|
||||
@@ -458,6 +465,38 @@ impl ThreadStore for LocalThreadStore {
|
||||
Box::pin(async move { list_threads::list_threads(self, params).await })
|
||||
}
|
||||
|
||||
fn supports_thread_sections(&self) -> bool {
|
||||
self.state_db.is_some()
|
||||
}
|
||||
|
||||
fn list_thread_sections(
|
||||
&self,
|
||||
params: ListThreadSectionsParams,
|
||||
) -> ThreadStoreFuture<'_, StoredThreadSectionsPage> {
|
||||
Box::pin(async move { thread_sections::list_thread_sections(self, params).await })
|
||||
}
|
||||
|
||||
fn create_thread_section(
|
||||
&self,
|
||||
params: CreateThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, StoredThreadSection> {
|
||||
Box::pin(async move { thread_sections::create_thread_section(self, params).await })
|
||||
}
|
||||
|
||||
fn rename_thread_section(
|
||||
&self,
|
||||
params: RenameThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, Option<StoredThreadSection>> {
|
||||
Box::pin(async move { thread_sections::rename_thread_section(self, params).await })
|
||||
}
|
||||
|
||||
fn delete_thread_section(
|
||||
&self,
|
||||
params: DeleteThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, bool> {
|
||||
Box::pin(async move { thread_sections::delete_thread_section(self, params).await })
|
||||
}
|
||||
|
||||
fn supports_paginated_history_lists(&self) -> bool {
|
||||
self.state_db.is_some()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
use super::LocalThreadStore;
|
||||
use crate::CreateThreadSectionParams;
|
||||
use crate::DeleteThreadSectionParams;
|
||||
use crate::ListThreadSectionsParams;
|
||||
use crate::RenameThreadSectionParams;
|
||||
use crate::StoredThreadSection;
|
||||
use crate::StoredThreadSectionsPage;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::ThreadStoreResult;
|
||||
use codex_rollout::StateDbHandle;
|
||||
|
||||
pub(super) async fn list_thread_sections(
|
||||
store: &LocalThreadStore,
|
||||
params: ListThreadSectionsParams,
|
||||
) -> ThreadStoreResult<StoredThreadSectionsPage> {
|
||||
let state_db = state_db(store, "threadSection/list")?;
|
||||
let page = state_db
|
||||
.list_thread_sections(params.cursor.as_deref(), params.limit)
|
||||
.await
|
||||
.map_err(|err| section_error("list", err))?;
|
||||
|
||||
Ok(StoredThreadSectionsPage {
|
||||
sections: page
|
||||
.sections
|
||||
.into_iter()
|
||||
.map(|section| StoredThreadSection {
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
})
|
||||
.collect(),
|
||||
next_cursor: page.next_cursor,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn create_thread_section(
|
||||
store: &LocalThreadStore,
|
||||
params: CreateThreadSectionParams,
|
||||
) -> ThreadStoreResult<StoredThreadSection> {
|
||||
let state_db = state_db(store, "threadSection/create")?;
|
||||
let section = state_db
|
||||
.create_thread_section(¶ms.name)
|
||||
.await
|
||||
.map_err(|err| section_error("create", err))?;
|
||||
|
||||
Ok(StoredThreadSection {
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn rename_thread_section(
|
||||
store: &LocalThreadStore,
|
||||
params: RenameThreadSectionParams,
|
||||
) -> ThreadStoreResult<Option<StoredThreadSection>> {
|
||||
let state_db = state_db(store, "threadSection/update")?;
|
||||
let section = state_db
|
||||
.rename_thread_section(¶ms.section_id, ¶ms.name)
|
||||
.await
|
||||
.map_err(|err| section_error("update", err))?;
|
||||
|
||||
Ok(section.map(|section| StoredThreadSection {
|
||||
id: section.id,
|
||||
name: section.name,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn delete_thread_section(
|
||||
store: &LocalThreadStore,
|
||||
params: DeleteThreadSectionParams,
|
||||
) -> ThreadStoreResult<bool> {
|
||||
let state_db = state_db(store, "threadSection/delete")?;
|
||||
state_db
|
||||
.delete_thread_section(¶ms.section_id)
|
||||
.await
|
||||
.map_err(|err| section_error("delete", err))
|
||||
}
|
||||
|
||||
fn state_db<'store>(
|
||||
store: &'store LocalThreadStore,
|
||||
operation: &'static str,
|
||||
) -> ThreadStoreResult<&'store StateDbHandle> {
|
||||
store
|
||||
.state_db
|
||||
.as_ref()
|
||||
.ok_or(ThreadStoreError::Unsupported { operation })
|
||||
}
|
||||
|
||||
fn section_error(operation: &str, error: impl std::fmt::Display) -> ThreadStoreError {
|
||||
ThreadStoreError::Internal {
|
||||
message: format!("failed to {operation} thread section: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "thread_sections_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,76 @@
|
||||
use super::LocalThreadStore;
|
||||
use crate::CreateThreadSectionParams;
|
||||
use crate::DeleteThreadSectionParams;
|
||||
use crate::ListThreadSectionsParams;
|
||||
use crate::RenameThreadSectionParams;
|
||||
use crate::StoredThreadSection;
|
||||
use crate::ThreadStore;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::local::test_support::test_config;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_thread_sections_require_sqlite_and_preserve_section_identity() {
|
||||
let home = tempfile::TempDir::new().expect("temp dir");
|
||||
let config = test_config(home.path());
|
||||
let unsupported_store = LocalThreadStore::new(config.clone(), /*state_db*/ None);
|
||||
assert!(!unsupported_store.supports_thread_sections());
|
||||
assert!(matches!(
|
||||
unsupported_store
|
||||
.create_thread_section(CreateThreadSectionParams {
|
||||
name: "Research".to_string(),
|
||||
})
|
||||
.await,
|
||||
Err(ThreadStoreError::Unsupported {
|
||||
operation: "threadSection/create"
|
||||
})
|
||||
));
|
||||
|
||||
let runtime = codex_state::StateRuntime::init(
|
||||
config.sqlite.clone(),
|
||||
config.default_model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
let store = LocalThreadStore::new(config, Some(runtime));
|
||||
assert!(store.supports_thread_sections());
|
||||
|
||||
let section = store
|
||||
.create_thread_section(CreateThreadSectionParams {
|
||||
name: "Research".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("create section");
|
||||
let sections = store
|
||||
.list_thread_sections(ListThreadSectionsParams {
|
||||
cursor: None,
|
||||
limit: 10,
|
||||
})
|
||||
.await
|
||||
.expect("list sections");
|
||||
assert!(sections.sections.contains(§ion));
|
||||
|
||||
let renamed = store
|
||||
.rename_thread_section(RenameThreadSectionParams {
|
||||
section_id: section.id.clone(),
|
||||
name: "Planning".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("rename section");
|
||||
assert_eq!(
|
||||
renamed,
|
||||
Some(StoredThreadSection {
|
||||
id: section.id.clone(),
|
||||
name: "Planning".to_string(),
|
||||
})
|
||||
);
|
||||
|
||||
assert!(
|
||||
store
|
||||
.delete_thread_section(DeleteThreadSectionParams {
|
||||
section_id: section.id,
|
||||
})
|
||||
.await
|
||||
.expect("delete section")
|
||||
);
|
||||
}
|
||||
@@ -8,10 +8,13 @@ use crate::AppendThreadItemsParams;
|
||||
use crate::ArchiveThreadParams;
|
||||
use crate::ArchiveThreadsParams;
|
||||
use crate::CreateThreadParams;
|
||||
use crate::CreateThreadSectionParams;
|
||||
use crate::DeleteThreadParams;
|
||||
use crate::DeleteThreadSectionParams;
|
||||
use crate::DeleteThreadsParams;
|
||||
use crate::ItemPage;
|
||||
use crate::ListItemsParams;
|
||||
use crate::ListThreadSectionsParams;
|
||||
use crate::ListThreadsParams;
|
||||
use crate::ListTurnsParams;
|
||||
use crate::LoadThreadHistoryParams;
|
||||
@@ -20,12 +23,15 @@ use crate::PrepareForkParams;
|
||||
use crate::PreparedFork;
|
||||
use crate::ReadThreadByRolloutPathParams;
|
||||
use crate::ReadThreadParams;
|
||||
use crate::RenameThreadSectionParams;
|
||||
use crate::ResumeThreadParams;
|
||||
use crate::SearchThreadOccurrencesParams;
|
||||
use crate::SearchThreadsParams;
|
||||
use crate::StoredModelContext;
|
||||
use crate::StoredThread;
|
||||
use crate::StoredThreadHistory;
|
||||
use crate::StoredThreadSection;
|
||||
use crate::StoredThreadSectionsPage;
|
||||
use crate::ThreadOccurrenceSearchPage;
|
||||
use crate::ThreadPage;
|
||||
use crate::ThreadSearchPage;
|
||||
@@ -123,6 +129,59 @@ pub trait ThreadStore: Any + Send + Sync {
|
||||
/// Lists stored threads matching the supplied filters.
|
||||
fn list_threads(&self, params: ListThreadsParams) -> ThreadStoreFuture<'_, ThreadPage>;
|
||||
|
||||
/// Whether this store can discover and manage independently persisted thread sections.
|
||||
fn supports_thread_sections(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Lists independently persisted thread sections.
|
||||
fn list_thread_sections(
|
||||
&self,
|
||||
_params: ListThreadSectionsParams,
|
||||
) -> ThreadStoreFuture<'_, StoredThreadSectionsPage> {
|
||||
Box::pin(async {
|
||||
Err(ThreadStoreError::Unsupported {
|
||||
operation: "threadSection/list",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a custom thread section with a stable, server-assigned identity.
|
||||
fn create_thread_section(
|
||||
&self,
|
||||
_params: CreateThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, StoredThreadSection> {
|
||||
Box::pin(async {
|
||||
Err(ThreadStoreError::Unsupported {
|
||||
operation: "threadSection/create",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Renames a custom thread section, returning `None` when it does not exist.
|
||||
fn rename_thread_section(
|
||||
&self,
|
||||
_params: RenameThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, Option<StoredThreadSection>> {
|
||||
Box::pin(async {
|
||||
Err(ThreadStoreError::Unsupported {
|
||||
operation: "threadSection/update",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Deletes a custom thread section and reports whether it existed.
|
||||
fn delete_thread_section(
|
||||
&self,
|
||||
_params: DeleteThreadSectionParams,
|
||||
) -> ThreadStoreFuture<'_, bool> {
|
||||
Box::pin(async {
|
||||
Err(ThreadStoreError::Unsupported {
|
||||
operation: "threadSection/delete",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether paginated threads can hydrate durable history through turn and item lists.
|
||||
fn supports_paginated_history_lists(&self) -> bool {
|
||||
false
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/// Parameters for listing independently persisted thread sections.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ListThreadSectionsParams {
|
||||
/// Opaque cursor returned by a previous section listing.
|
||||
pub cursor: Option<String>,
|
||||
/// Maximum number of sections to return.
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
/// Parameters for creating a thread section.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CreateThreadSectionParams {
|
||||
/// User-facing section name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Parameters for renaming a thread section.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RenameThreadSectionParams {
|
||||
/// Stable identifier of the section to rename.
|
||||
pub section_id: String,
|
||||
/// Replacement user-facing section name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Parameters for deleting a thread section.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DeleteThreadSectionParams {
|
||||
/// Stable identifier of the section to delete.
|
||||
pub section_id: String,
|
||||
}
|
||||
|
||||
/// An independently persisted thread section and its user-facing name.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredThreadSection {
|
||||
/// Stable, server-owned section identifier.
|
||||
pub id: String,
|
||||
/// User-facing section name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// A cursor-paginated page of independently persisted thread sections.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredThreadSectionsPage {
|
||||
/// Sections returned for this page.
|
||||
pub sections: Vec<StoredThreadSection>,
|
||||
/// Opaque cursor to continue listing sections.
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
Reference in New Issue
Block a user