Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion codex-rs/app-server-protocol/src/protocol/v2/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1088,10 +1088,15 @@ pub struct ThreadListParams {
/// Optional substring filter for the extracted thread title.
#[ts(optional = nullable)]
pub search_term: Option<String>,
/// Optional direct parent thread filter.
/// Optional direct parent thread filter. Mutually exclusive with `ancestorThreadId`.
#[experimental("thread/list.parentThreadId")]
#[ts(optional = nullable)]
pub parent_thread_id: Option<String>,
/// Optional ancestor thread filter. Returns spawned descendants at any depth, excluding the
/// ancestor itself. Mutually exclusive with `parentThreadId`.
#[experimental("thread/list.ancestorThreadId")]
#[ts(optional = nullable)]
pub ancestor_thread_id: Option<String>,
Comment thread
btraut-openai marked this conversation as resolved.
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server-test-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,7 @@ async fn thread_list(endpoint: &Endpoint, config_overrides: &[String], limit: u3
source_kinds: None,
archived: None,
parent_thread_id: None,
ancestor_thread_id: None,
cwd: None,
use_state_db_only: false,
search_term: None,
Expand Down
11 changes: 6 additions & 5 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ Example with notification opt-out:
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. Multi-agent mode restores the last effective mode from rollout history when available; clients can select another mode on the first `turn/start`.
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. `instructionSources` lists loaded instruction files using each source environment's native absolute path syntax, including files loaded from remote environments. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. Their experimental `multiAgentMode` field, and the corresponding thread setting, report the thread's current mode. Turn construction separately determines whether that mode is applicable to the selected model and runtime configuration.
- `thread/list` — page through stored threads; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Experimental clients can use `parentThreadId` to filter direct spawned children represented by persisted spawn-edge state. Review and Guardian threads are not included because they do not participate in that spawn-edge lifecycle. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate parent is known.
- `thread/list` — page through stored threads; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Experimental clients can use `parentThreadId` for direct spawned children or `ancestorThreadId` for spawned descendants at any depth; the two filters are mutually exclusive. Review and Guardian threads are not included because they do not participate in that spawn-edge lifecycle. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. Subagent threads also include `parentThreadId` when the immediate parent is known.
- `thread/loaded/list` — list the thread ids currently loaded in memory.
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
- `thread/turns/list` — experimental; page through a stored thread’s turn history without resuming it; supports cursor-based pagination with `sortDirection`, `itemsView`, `nextCursor`, and `backwardsCursor`.
Expand Down Expand Up @@ -403,18 +403,19 @@ Example:

When `nextCursor` is `null`, you’ve reached the final page.

### Example: List direct child threads
### Example: List descendant threads

Enable `capabilities.experimentalApi` during initialization, then use `thread/list` with `parentThreadId` to page through a thread's direct spawned children from persisted spawn-edge state. Results do not recursively include grandchildren. Review and Guardian threads are not included because they do not participate in the spawn-edge lifecycle. When `modelProviders` or `sourceKinds` is omitted, parent-filtered requests include every provider or source kind, respectively. Explicit filters retain the ordinary `thread/list` behavior, including the interactive-only default for an empty `sourceKinds` list.
Enable `capabilities.experimentalApi` during initialization, then use `thread/list` with `ancestorThreadId` to page through every spawned descendant of a thread from persisted spawn-edge state. The ancestor itself is excluded, and each result's `parentThreadId` remains its immediate parent. Use `parentThreadId` instead when only direct children are wanted; sending both filters is invalid. Review and Guardian threads are not included because they do not participate in the spawn-edge lifecycle. When `modelProviders` or `sourceKinds` is omitted, relationship-filtered requests include every provider or source kind, respectively. Explicit filters retain the ordinary `thread/list` behavior, including the interactive-only default for an empty `sourceKinds` list.

```json
{ "method": "thread/list", "id": 21, "params": {
"parentThreadId": "00000000-0000-0000-0000-000000000100",
"ancestorThreadId": "00000000-0000-0000-0000-000000000100",
"limit": 25
} }
{ "id": 21, "result": {
"data": [
{ "id": "00000000-0000-0000-0000-000000000101", "parentThreadId": "00000000-0000-0000-0000-000000000100", "status": { "type": "notLoaded" } }
{ "id": "00000000-0000-0000-0000-000000000101", "parentThreadId": "00000000-0000-0000-0000-000000000100", "status": { "type": "notLoaded" } },
{ "id": "00000000-0000-0000-0000-000000000102", "parentThreadId": "00000000-0000-0000-0000-000000000101", "status": { "type": "notLoaded" } }
],
"nextCursor": null,
"backwardsCursor": null
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/request_processors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ use codex_thread_store::SearchThreadsParams as StoreSearchThreadsParams;
use codex_thread_store::SortDirection as StoreSortDirection;
use codex_thread_store::StoredThread;
use codex_thread_store::ThreadMetadataPatch as StoreThreadMetadataPatch;
use codex_thread_store::ThreadRelationFilter as StoreThreadRelationFilter;
use codex_thread_store::ThreadSortKey as StoreThreadSortKey;
use codex_thread_store::ThreadStore;
use codex_thread_store::ThreadStoreError;
Expand Down
34 changes: 23 additions & 11 deletions codex-rs/app-server/src/request_processors/thread_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ struct ThreadListFilters {
cwd_filters: Option<Vec<PathBuf>>,
search_term: Option<String>,
use_state_db_only: bool,
parent_thread_id: Option<ThreadId>,
relation_filter: Option<StoreThreadRelationFilter>,
}

fn collect_resume_override_mismatches(
Expand Down Expand Up @@ -1904,13 +1904,25 @@ impl ThreadRequestProcessor {
use_state_db_only,
search_term,
parent_thread_id,
ancestor_thread_id,
} = params;
let cwd_filters = normalize_thread_list_cwd_filters(cwd)?;
let parent_thread_id = parent_thread_id
.as_deref()
.map(ThreadId::from_string)
.transpose()
.map_err(|err| invalid_request(format!("invalid parent thread id: {err}")))?;
let relation_filter = match (parent_thread_id, ancestor_thread_id) {
(Some(_), Some(_)) => {
return Err(invalid_request(
"parentThreadId and ancestorThreadId are mutually exclusive",
));
}
(Some(parent_thread_id), None) => Some(StoreThreadRelationFilter::DirectChildrenOf(
ThreadId::from_string(&parent_thread_id)
.map_err(|err| invalid_request(format!("invalid parent thread id: {err}")))?,
)),
(None, Some(ancestor_thread_id)) => Some(StoreThreadRelationFilter::DescendantsOf(
ThreadId::from_string(&ancestor_thread_id)
.map_err(|err| invalid_request(format!("invalid ancestor thread id: {err}")))?,
)),
(None, None) => None,
};

let requested_page_size = limit
.map(|value| value as usize)
Expand All @@ -1935,7 +1947,7 @@ impl ThreadRequestProcessor {
cwd_filters,
search_term,
use_state_db_only,
parent_thread_id,
relation_filter,
},
)
.await?;
Expand Down Expand Up @@ -3665,7 +3677,7 @@ impl ThreadRequestProcessor {
cwd_filters,
search_term,
use_state_db_only,
parent_thread_id,
relation_filter,
} = filters;
let mut cursor_obj = cursor;
let mut last_cursor = cursor_obj.clone();
Expand All @@ -3681,11 +3693,11 @@ impl ThreadRequestProcessor {
Some(providers)
}
}
None if parent_thread_id.is_some() => None,
None if relation_filter.is_some() => None,
None => Some(vec![self.config.model_provider_id.clone()]),
};
let (allowed_sources_vec, source_kind_filter) =
if parent_thread_id.is_some() && source_kinds.is_none() {
if relation_filter.is_some() && source_kinds.is_none() {
(Vec::new(), None)
} else {
compute_source_filters(source_kinds)
Expand All @@ -3711,7 +3723,7 @@ impl ThreadRequestProcessor {
archived,
search_term: search_term.clone(),
use_state_db_only,
parent_thread_id,
relation_filter,
})
.await
.map_err(thread_store_list_error)?;
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/app-server/tests/suite/v2/external_agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
use_state_db_only: false,
search_term: None,
parent_thread_id: None,
ancestor_thread_id: None,
})
.await?;
let response: JSONRPCResponse = timeout(
Expand Down Expand Up @@ -892,6 +893,7 @@ required = true
use_state_db_only: false,
search_term: None,
parent_thread_id: None,
ancestor_thread_id: None,
})
.await?;
let response: JSONRPCResponse = timeout(
Expand Down Expand Up @@ -983,6 +985,7 @@ async fn external_agent_config_import_accepts_detected_session_payload_after_res
use_state_db_only: false,
search_term: None,
parent_thread_id: None,
ancestor_thread_id: None,
})
.await?;
let response: JSONRPCResponse = timeout(
Expand Down Expand Up @@ -1075,6 +1078,7 @@ async fn external_agent_config_import_skips_already_imported_session_versions()
use_state_db_only: false,
search_term: None,
parent_thread_id: None,
ancestor_thread_id: None,
})
.await?;
let response: JSONRPCResponse = timeout(
Expand Down Expand Up @@ -1214,6 +1218,7 @@ async fn external_agent_config_import_returns_before_background_session_import_f
use_state_db_only: false,
search_term: None,
parent_thread_id: None,
ancestor_thread_id: None,
})
.await?;
let response: JSONRPCResponse = timeout(
Expand Down Expand Up @@ -1343,6 +1348,7 @@ async fn external_agent_config_import_compacts_huge_session_before_first_follow_
use_state_db_only: false,
search_term: None,
parent_thread_id: None,
ancestor_thread_id: None,
})
.await?;
let response: JSONRPCResponse = timeout(
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/remote_thread_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ async fn thread_delete_with_non_local_thread_store_does_not_create_local_persist
use_state_db_only: false,
search_term: None,
parent_thread_id: None,
ancestor_thread_id: None,
},
})
.await?
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/thread_fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ async fn list_threads(mcp: &mut TestAppServer) -> Result<ThreadListResponse> {
use_state_db_only: false,
search_term: None,
parent_thread_id: None,
ancestor_thread_id: None,
})
.await?;
let list_resp: JSONRPCResponse = timeout(
Expand Down
Loading
Loading