Add persisted thread pinning to the app server (#34840)

## What changed

- Add `isPinned` to thread responses and allow `thread/metadata/update` to pin or unpin stored threads.
- Add an `isPinned` filter to `thread/list`, including cursor-based pagination and combinations with relationship filters.
- Persist pin state in SQLite without modifying rollout files, default existing threads to unpinned, and preserve pins through reconciliation and archive transitions.

## Testing

- Cover protocol serialization, database migration and indexing, pin updates, filtered pagination, rollout reconciliation, and archive/unarchive behavior.

GitOrigin-RevId: c2eacabd6dae465e2ae6ce8a169e00740960cdb7
This commit is contained in:
joeytrasatti-openai authored and copyberry committed 2026-07-22 22:54:28 +00:00
1 parent d7e8f4c3dc
commit 400ee190c3
71 files changed
+1058 -60

No files matched your search

+21 -11
View File
@@ -97,15 +97,17 @@ pub(super) async fn list_rollout_threads(
sort_key: codex_rollout::ThreadSortKey,
sort_direction: codex_rollout::SortDirection,
) -> ThreadStoreResult<codex_rollout::ThreadsPage> {
if let Some(relation_filter) = params.relation_filter {
let relation_filter = match relation_filter {
ThreadRelationFilter::DirectChildrenOf(parent_thread_id) => {
codex_state::ThreadRelationFilter::DirectChildrenOf(parent_thread_id)
}
ThreadRelationFilter::DescendantsOf(ancestor_thread_id) => {
codex_state::ThreadRelationFilter::DescendantsOf(ancestor_thread_id)
}
};
if params.relation_filter.is_some() || params.is_pinned.is_some() {
let relation_filter = params
.relation_filter
.map(|relation_filter| match relation_filter {
ThreadRelationFilter::DirectChildrenOf(parent_thread_id) => {
codex_state::ThreadRelationFilter::DirectChildrenOf(parent_thread_id)
}
ThreadRelationFilter::DescendantsOf(ancestor_thread_id) => {
codex_state::ThreadRelationFilter::DescendantsOf(ancestor_thread_id)
}
});
let page = codex_rollout::state_db::list_threads_db(
state_db.as_deref(),
config.codex_home.as_path(),
@@ -116,13 +118,14 @@ pub(super) async fn list_rollout_threads(
params.allowed_sources.as_slice(),
params.model_providers.as_deref(),
params.cwd_filters.as_deref(),
Some(relation_filter),
relation_filter,
params.archived,
params.is_pinned,
params.search_term.as_deref(),
)
.await
.ok_or_else(|| ThreadStoreError::Internal {
message: "state DB unavailable for relationship-filtered thread listing".to_string(),
message: "state DB unavailable for filtered thread listing".to_string(),
})?;
return Ok(page.into());
}
@@ -236,6 +239,7 @@ mod tests {
allowed_sources: Vec::new(),
model_providers: None,
cwd_filters: None,
is_pinned: None,
archived: false,
search_term: None,
relation_filter: None,
@@ -296,6 +300,7 @@ mod tests {
allowed_sources: Vec::new(),
model_providers: None,
cwd_filters: None,
is_pinned: None,
archived: false,
search_term: Some("needle".to_string()),
relation_filter: None,
@@ -368,6 +373,7 @@ mod tests {
allowed_sources: Vec::new(),
model_providers: None,
cwd_filters: None,
is_pinned: None,
archived: false,
search_term: Some("canonical".to_string()),
relation_filter: None,
@@ -404,6 +410,7 @@ mod tests {
allowed_sources: Vec::new(),
model_providers: None,
cwd_filters: None,
is_pinned: None,
archived: false,
search_term: None,
relation_filter: None,
@@ -420,6 +427,7 @@ mod tests {
allowed_sources: Vec::new(),
model_providers: None,
cwd_filters: None,
is_pinned: None,
archived: true,
search_term: None,
relation_filter: None,
@@ -472,6 +480,7 @@ mod tests {
allowed_sources: vec![SessionSource::Cli],
model_providers: Some(vec!["test-provider".to_string()]),
cwd_filters: None,
is_pinned: None,
archived: false,
search_term: None,
relation_filter: None,
@@ -509,6 +518,7 @@ mod tests {
allowed_sources: Vec::new(),
model_providers: None,
cwd_filters: None,
is_pinned: None,
archived: false,
search_term: None,
relation_filter: None,