Scope local history discovery to the current ChatGPT account (#58918)

## What changed

- Add `currentAccountOnly` to `thread/list` and `thread/search`, defaulting to `false`, and enable it for TUI history discovery, resume pickers, and task mentions.
- Mark newly created threads as account-scoped and require each recorded creator user or account ID to match the current login. Older threads and threads without creator IDs remain shared; API-key logins continue to see all local history.
- Preserve account scope through resume and revert, including fallback to original rollout metadata when an older binary drops the marker. Forks receive new-thread scope.
- Skip unreadable rollout headers in scoped discovery and filter search candidates before reading snippets. Direct access by thread ID remains available.

## Testing

Add coverage for account matching, shared legacy history, list page refilling, search pagination, API-key visibility, and scope preservation across reverts. Extend rollout lookup coverage to compressed active and archived files.

GitOrigin-RevId: aa374f5e69165be5a71c0716ded1b105f5fb51e0
This commit is contained in:
jif
2026-11-02 16:00:20 +00:00
committed by copyberry
parent 6c4fa86838
commit 8806313db4
55 changed files with 559 additions and 15 deletions
@@ -5048,6 +5048,10 @@
"null"
]
},
"currentAccountOnly": {
"description": "Show local history visible to the current ChatGPT account. Defaults to false.",
"type": "boolean"
},
"cursor": {
"description": "Opaque pagination cursor returned by a previous call.",
"type": [
@@ -24345,6 +24345,10 @@
"null"
]
},
"currentAccountOnly": {
"description": "Show local history visible to the current ChatGPT account. Defaults to false.",
"type": "boolean"
},
"cursor": {
"description": "Opaque pagination cursor returned by a previous call.",
"type": [
@@ -22048,6 +22048,10 @@
"null"
]
},
"currentAccountOnly": {
"description": "Show local history visible to the current ChatGPT account. Defaults to false.",
"type": "boolean"
},
"cursor": {
"description": "Opaque pagination cursor returned by a previous call.",
"type": [
@@ -54,6 +54,10 @@
"null"
]
},
"currentAccountOnly": {
"description": "Show local history visible to the current ChatGPT account. Defaults to false.",
"type": "boolean"
},
"cursor": {
"description": "Opaque pagination cursor returned by a previous call.",
"type": [
@@ -6,6 +6,9 @@ import type { ThreadSortKey } from "./ThreadSortKey";
import type { ThreadSourceKind } from "./ThreadSourceKind";
export type ThreadListParams = {/**
* Show local history visible to the current ChatGPT account. Defaults to false.
*/
currentAccountOnly?: boolean, /**
* Opaque pagination cursor returned by a previous call.
*/
cursor?: string | null, /**
@@ -1427,6 +1427,9 @@ pub struct ThreadSectionDeleteResponse {}
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadListParams {
/// Show local history visible to the current ChatGPT account. Defaults to false.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub current_account_only: bool,
/// Opaque pagination cursor returned by a previous call.
#[ts(optional = nullable)]
pub cursor: Option<String>,
@@ -1515,6 +1518,9 @@ pub struct ThreadListParams {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ThreadSearchParams {
/// Show local history visible to the current ChatGPT account. Defaults to false.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub current_account_only: bool,
/// Opaque pagination cursor returned by a previous call.
#[ts(optional = nullable)]
pub cursor: Option<String>,
@@ -1167,6 +1167,7 @@ async fn thread_list(endpoint: &Endpoint, config_overrides: &[String], limit: u3
println!("< initialize response: {initialize:?}");
let response = client.thread_list(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
+10
View File
@@ -23,6 +23,16 @@ resolved memory version, including thread-specific configuration overrides.
It is `null` when memory use is disabled or the selected summary is missing,
unreadable, or blank. It never falls back to the other memory version.
# Local history by account
Pass `currentAccountOnly: true` on `thread/list` or `thread/search` to scope local
discovery to the current ChatGPT login. Newly created threads record that discovery
is account-scoped, and each recorded creator user or account ID must match.
Threads created before this change and threads without creator IDs stay visible
across account switches. Resume and revert preserve that distinction; forks are new threads.
Scoped requests skip candidates whose rollout header cannot be read. API-key
logins see all local history. Direct access by thread ID remains available.
# Thread list exclusions
`thread/list` accepts `excludedThreadIds`, an optional array of up to 100
@@ -46,6 +46,7 @@ use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::error::CodexErrorDetails;
use codex_protocol::mcp::ClientMcpExtensions;
use codex_protocol::protocol::ThreadHistoryMode;
use codex_thread_store::LocalHistoryAccount;
use codex_thread_store::PersistContext;
use std::ops::ControlFlow;
@@ -91,6 +92,7 @@ async fn remove_pending_thread_metadata(
}
struct ThreadListFilters {
account: Option<LocalHistoryAccount>,
model_providers: Option<Vec<String>>,
source_kinds: Option<Vec<ThreadSourceKind>>,
archived: bool,
@@ -2739,6 +2741,7 @@ impl ThreadRequestProcessor {
params: ThreadListParams,
) -> Result<ThreadListResponse, JSONRPCErrorError> {
let ThreadListParams {
current_account_only,
cursor,
limit,
excluded_thread_ids,
@@ -2860,6 +2863,7 @@ impl ThreadRequestProcessor {
store_sort_key,
sort_direction,
ThreadListFilters {
account: self.local_history_account(current_account_only),
model_providers,
source_kinds,
archived: archived.unwrap_or(false),
@@ -2909,6 +2913,7 @@ impl ThreadRequestProcessor {
params: ThreadSearchParams,
) -> Result<ThreadSearchResponse, JSONRPCErrorError> {
let ThreadSearchParams {
current_account_only,
cursor,
limit,
sort_key,
@@ -2935,6 +2940,7 @@ impl ThreadRequestProcessor {
let mut cursor_obj = cursor;
let mut last_cursor = cursor_obj.clone();
let mut remaining = requested_page_size;
let account = self.local_history_account(current_account_only);
let mut search_results = Vec::with_capacity(requested_page_size);
let mut next_cursor = None;
@@ -2942,6 +2948,7 @@ impl ThreadRequestProcessor {
let page = self
.thread_store
.search_threads(StoreSearchThreadsParams {
account: account.clone(),
page_size: remaining.min(THREAD_LIST_MAX_LIMIT),
cursor: cursor_obj.clone(),
sort_key: store_sort_key,
@@ -5996,6 +6003,22 @@ impl ThreadRequestProcessor {
Ok(GetConversationSummaryResponse { summary })
}
/// Local discovery matches each recorded creator ID to the current login. Threads with
/// no creator IDs or predating enforcement are shared; API-key logins see all local history.
fn local_history_account(&self, current_account_only: bool) -> Option<LocalHistoryAccount> {
if !current_account_only || !self.thread_store.as_any().is::<LocalThreadStore>() {
return None;
}
let auth = self.auth_manager.auth_cached();
if auth.as_ref().is_some_and(CodexAuth::is_api_key_auth) {
return None;
}
Some(LocalHistoryAccount {
user_id: auth.as_ref().and_then(CodexAuth::get_chatgpt_user_id),
account_id: auth.as_ref().and_then(CodexAuth::get_account_id),
})
}
async fn list_threads_common(
&self,
requested_page_size: usize,
@@ -6005,6 +6028,7 @@ impl ThreadRequestProcessor {
filters: ThreadListFilters,
) -> Result<(Vec<StoredThread>, Option<String>), JSONRPCErrorError> {
let ThreadListFilters {
account,
model_providers,
source_kinds,
archived,
@@ -6067,8 +6091,23 @@ impl ThreadRequestProcessor {
.await
.map_err(thread_store_list_error)?;
let page_thread_ids: Vec<_> = page.items.iter().map(|item| item.thread_id).collect();
let mut original_paths = None;
let mut filtered = Vec::with_capacity(page.items.len());
for it in page.items {
if let Some(account) = &account
&& !account
.includes(
&self.config.codex_home,
it.thread_id,
it.rollout_path.as_deref(),
&page_thread_ids,
&mut original_paths,
)
.await
{
continue;
}
let source = with_thread_spawn_agent_metadata(
it.source.clone(),
it.agent_nickname.clone(),
@@ -254,6 +254,7 @@ fn create_fake_rollout_with_source_and_parent_thread_id(
// Build JSONL lines
let meta = SessionMeta {
account_scoped: false,
attached_thread: false,
creator_user_id: None,
creator_account_id: None,
@@ -351,6 +352,7 @@ pub fn create_fake_rollout_with_text_elements(
// Build JSONL lines
let meta = SessionMeta {
account_scoped: false,
attached_thread: false,
creator_user_id: None,
creator_account_id: None,
@@ -610,6 +610,7 @@ source = {:?}
let request_id = mcp
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -2066,6 +2067,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
let request_id = mcp
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -2259,6 +2261,7 @@ required = true
let request_id = mcp
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -2347,6 +2350,7 @@ async fn external_agent_config_import_accepts_detected_session_payload_after_res
let request_id = mcp
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -2432,6 +2436,7 @@ async fn external_agent_config_import_skips_already_imported_session_versions()
let request_id = mcp
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -2563,6 +2568,7 @@ async fn external_agent_config_import_returns_before_background_session_import_f
let request_id = mcp
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -2682,6 +2688,7 @@ async fn external_agent_config_import_compacts_huge_session_before_first_follow_
let request_id = mcp
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -100,6 +100,7 @@ async fn projects_list_by_recency_and_preserve_metadata_timestamps() -> Result<(
.request(|request_id| ClientRequest::ThreadList {
request_id,
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -326,6 +327,7 @@ async fn projects_persist_and_assign_threads() -> Result<()> {
.request(|request_id| ClientRequest::ThreadList {
request_id,
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -375,6 +377,7 @@ async fn projects_persist_and_assign_threads() -> Result<()> {
.request(|request_id| ClientRequest::ThreadList {
request_id,
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -427,6 +430,7 @@ async fn projects_persist_and_assign_threads() -> Result<()> {
.request(|request_id| ClientRequest::ThreadList {
request_id,
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -624,6 +628,7 @@ async fn projects_persist_and_assign_threads() -> Result<()> {
.request(|request_id| ClientRequest::ThreadList {
request_id,
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -717,6 +722,7 @@ async fn deleted_project_is_dropped_before_first_durable_thread_persistence() ->
.request(|request_id| ClientRequest::ThreadList {
request_id,
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -909,6 +915,7 @@ async fn projects_validate_filters_cursors_and_sqlite_less_assignment() -> Resul
for project_id in [String::new(), Uuid::now_v7().to_string()] {
let request_id = server
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -997,6 +1004,7 @@ async fn projects_validate_filters_cursors_and_sqlite_less_assignment() -> Resul
assert_eq!(error.error.code, -32601);
let list_id = unsupported_projects
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -254,6 +254,7 @@ async fn thread_list_repeated_cursor_returns_internal_error() -> Result<()> {
.request(ClientRequest::ThreadList {
request_id: RequestId::Integer(1),
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -313,6 +314,7 @@ async fn thread_list_repeated_cursor_returns_internal_error() -> Result<()> {
.request(ClientRequest::ThreadList {
request_id: RequestId::Integer(2),
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -496,6 +498,7 @@ async fn thread_delete_with_non_local_thread_store_does_not_create_local_persist
.request(ClientRequest::ThreadList {
request_id: RequestId::Integer(3),
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -159,6 +159,7 @@ async fn state_recovery_restores_saved_threads() -> Result<()> {
.await?;
let request = server
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -116,6 +116,7 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs
async fn list_threads(mcp: &mut TestAppServer) -> Result<ThreadListResponse> {
let list_id = mcp
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -238,6 +239,20 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
let thread_path = thread.path.clone().expect("thread path");
assert!(thread_path.as_path().is_absolute());
assert_ne!(thread_path.as_path(), original_path);
assert_eq!(
(
read_session_meta_line(&original_path)
.await?
.meta
.account_scoped,
read_session_meta_line(thread_path.as_path())
.await?
.meta
.account_scoped,
),
(false, true),
"forking legacy history creates a newly account-scoped thread"
);
assert!(thread.cwd.as_path().is_absolute());
assert_eq!(thread.source, SessionSource::VsCode);
assert_eq!(thread.thread_source, Some(ThreadSource::User));
@@ -102,6 +102,7 @@ async fn list_threads_with_sort(
mcp.request(|request_id| ClientRequest::ThreadList {
request_id,
params: codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -144,6 +145,7 @@ async fn list_threads_for_relation(
mcp.request(|request_id| ClientRequest::ThreadList {
request_id,
params: codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -777,6 +779,7 @@ async fn thread_list_respects_cwd_filters() -> Result<()> {
let mut mcp = init_mcp(codex_home.path()).await?;
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -893,6 +896,7 @@ sqlite = true
let mut mcp = init_mcp(codex_home.path()).await?;
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -964,6 +968,7 @@ async fn thread_search_returns_content_matches() -> Result<()> {
let mut mcp = init_mcp(codex_home.path()).await?;
let request_id = mcp
.send_thread_search_request(codex_app_server_protocol::ThreadSearchParams {
current_account_only: false,
cursor: None,
limit: Some(10),
sort_key: None,
@@ -1019,6 +1024,7 @@ async fn thread_search_returns_content_matches() -> Result<()> {
let request_id = mcp
.send_thread_search_request(codex_app_server_protocol::ThreadSearchParams {
current_account_only: false,
cursor: None,
limit: Some(10),
sort_key: None,
@@ -1081,6 +1087,7 @@ async fn thread_search_matches_json_escaped_content() -> Result<()> {
let mut mcp = init_mcp(codex_home.path()).await?;
let request_id = mcp
.send_thread_search_request(codex_app_server_protocol::ThreadSearchParams {
current_account_only: false,
cursor: None,
limit: Some(10),
sort_key: None,
@@ -1126,6 +1133,7 @@ async fn thread_search_filters_by_source_kind() -> Result<()> {
let mut mcp = init_mcp(codex_home.path()).await?;
let request_id = mcp
.send_thread_search_request(codex_app_server_protocol::ThreadSearchParams {
current_account_only: false,
cursor: None,
limit: Some(10),
sort_key: None,
@@ -1300,6 +1308,7 @@ sqlite = true
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -1339,6 +1348,7 @@ sqlite = true
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -1371,6 +1381,7 @@ sqlite = true
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -1559,6 +1570,7 @@ async fn thread_list_relation_filters_reject_invalid_requests() -> Result<()> {
let mut mcp = init_mcp(codex_home.path()).await?;
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -1588,6 +1600,7 @@ async fn thread_list_relation_filters_reject_invalid_requests() -> Result<()> {
let thread_id = ThreadId::new().to_string();
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -1828,6 +1841,7 @@ async fn thread_list_reports_loaded_subagent_direct_input_capability() -> Result
let request_id = mcp
.send_thread_search_request(codex_app_server_protocol::ThreadSearchParams {
current_account_only: false,
cursor: None,
limit: Some(10),
sort_key: None,
@@ -1881,6 +1895,7 @@ async fn thread_list_reports_loaded_subagent_direct_input_capability() -> Result
.request(|request_id| ClientRequest::ThreadList {
request_id,
params: codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -2705,6 +2720,7 @@ async fn thread_list_backwards_cursor_can_seed_forward_delta_sync() -> Result<()
} = {
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -2749,6 +2765,7 @@ async fn thread_list_backwards_cursor_can_seed_forward_delta_sync() -> Result<()
} = {
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -3183,6 +3200,7 @@ async fn thread_list_invalid_cursor_returns_error() -> Result<()> {
let request_id = mcp
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -3212,3 +3230,116 @@ async fn thread_list_invalid_cursor_returns_error() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn current_account_history_refills_list_and_search() -> Result<()> {
let home = TempDir::new()?;
create_minimal_config(home.path())?;
app_test_support::write_chatgpt_auth(
home.path(),
app_test_support::ChatGptAuthFixture::new("token")
.chatgpt_user_id("me")
.chatgpt_account_id("team")
.account_id("team"),
codex_config::types::AuthCredentialsStoreMode::File,
)?;
let (stamp, timestamp) = timestamp_at(
/*year*/ 2025, /*month*/ 1, /*day*/ 5, /*hour*/ 12, /*minute*/ 0,
/*second*/ 0,
);
let mut ids = Vec::new();
for _ in 0..6 {
ids.push(app_test_support::create_fake_paginated_rollout(
home.path(),
&stamp,
&timestamp,
"fixture-preview",
Some("openai"),
/*git_info*/ None,
)?);
}
// With tied creation times, the largest ID is first. Give it to the other user.
ids.sort_unstable_by(|a, b| b.cmp(a));
for (id, (user, account, preview, scoped)) in ids.iter().zip([
(Some("other-needle-user"), Some("team"), "needle", true),
(Some("me"), Some("team"), "needle", true),
(Some("me"), None, "partial", true),
(None, None, "shared", true),
(Some("legacy-user"), Some("legacy-team"), "needle", false),
(Some("me"), Some("other-team"), "other workspace", true),
]) {
let path = rollout_path(home.path(), &stamp, id);
let text = fs::read_to_string(&path)?;
let (header, rest) = text.split_once('\n').expect("session header");
let mut header: serde_json::Value = serde_json::from_str(header)?;
header["payload"]["creator_user_id"] = json!(user);
header["payload"]["creator_account_id"] = json!(account);
// Attribution predates enforcement: an absent marker keeps old history shared.
if scoped {
header["payload"]["account_scoped"] = json!(true);
}
let rest = rest.replace("fixture-preview", preview);
fs::write(path, format!("{header}\n{rest}"))?;
}
let mut server = init_mcp(home.path()).await?;
for (limit, db_only) in [(4, false), (4, true), (1, false), (1, true)] {
let params = json!({
"currentAccountOnly": true,
"sortKey": "created_at",
"limit": limit,
"useStateDbOnly": db_only,
});
let request = server.send_raw_request("thread/list", Some(params)).await?;
let list: ThreadListResponse = server.read_response(request).await?;
assert_eq!(
list.data.into_iter().map(|t| t.id).collect::<Vec<_>>(),
ids[1..=limit]
);
}
// The foreign header matches the raw search, but reading its snippet would hit invalid UTF-8.
let foreign = rollout_path(home.path(), &stamp, &ids[0]);
let intact = fs::read_to_string(&foreign)?;
let (header, rest) = intact.split_once('\n').expect("session header");
fs::write(
&foreign,
[header.as_bytes(), b"\n\xff\n", rest.as_bytes()].concat(),
)?;
let mut cursor = None;
for expected_id in [&ids[1], &ids[4]] {
let params = json!({
"currentAccountOnly": true,
"sortKey": "created_at",
"limit": 1,
"cursor": cursor,
"searchTerm": "needle",
});
let request = server
.send_raw_request("thread/search", Some(params))
.await?;
let search: ThreadSearchResponse = server.read_response(request).await?;
assert_eq!(
search
.data
.into_iter()
.map(|t| t.thread.id)
.collect::<Vec<_>>(),
vec![expected_id.clone()]
);
cursor = search.next_cursor;
}
assert_eq!(cursor, None);
fs::write(foreign, intact)?;
let request = server
.send_login_account_api_key_request("sk-test-key")
.await?;
let _: codex_app_server_protocol::LoginAccountResponse = server.read_response(request).await?;
let request = server
.send_raw_request("thread/list", Some(json!({"currentAccountOnly": true})))
.await?;
let list: ThreadListResponse = server.read_response(request).await?;
assert_eq!(list.data.len(), 6);
Ok(())
}
@@ -88,6 +88,7 @@ async fn thread_section_move_pins_and_archives_before_first_turn() -> Result<()>
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(move_id)).await??;
let list_params = ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -294,6 +295,7 @@ async fn thread_section_move_pins_and_unpins_with_filtered_recency_pagination()
}
let list_params = ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -488,6 +490,7 @@ async fn thread_sections_preserve_server_owned_manual_order_across_moves_and_res
}
let list_params = ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -411,6 +411,7 @@ async fn paginated_stored_thread_routes_projected_turns() -> Result<()> {
let list_id = mcp
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -1153,6 +1154,7 @@ async fn thread_list_includes_store_thread_without_rollout_path() -> Result<()>
.request(ClientRequest::ThreadList {
request_id: RequestId::Integer(1),
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -1536,6 +1538,7 @@ async fn paginated_thread_name_set_is_reflected_in_read_list_and_metadata_resume
// List should also surface the name.
let list_id = mcp
.send_thread_list_request(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -259,6 +259,14 @@ async fn thread_resume_paginated_model_context_preserves_original_metadata() ->
.await??;
timeout(DEFAULT_READ_TIMEOUT, primary.shutdown_gracefully()).await??;
assert!(
!codex_rollout::read_session_meta_line(&path)
.await?
.meta
.account_scoped,
"resuming legacy history must keep its cross-account visibility"
);
let mut secondary = TestAppServer::builder()
.with_codex_home(codex_home.path())
.without_auto_env()
@@ -4793,6 +4801,7 @@ async fn thread_resume_prefers_persisted_git_metadata_for_local_threads() -> Res
let rollout_dir = rollout_path.parent().expect("rollout parent directory");
std::fs::create_dir_all(rollout_dir)?;
let session_meta = SessionMeta {
account_scoped: false,
attached_thread: false,
creator_user_id: None,
creator_account_id: None,
@@ -4723,6 +4723,7 @@ async fn direct_input_to_multi_agent_v2_subagent_is_rejected(
.request(|request_id| ClientRequest::ThreadList {
request_id,
params: codex_app_server_protocol::ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
+1
View File
@@ -472,6 +472,7 @@ async fn remote_session_commands_with_workload_identity_use_server_auth() -> Res
.request_typed(ClientRequest::ThreadList {
request_id: RequestId::Integer(4),
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
cursor: None,
@@ -557,6 +557,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
fs::create_dir_all(parent).expect("should create rollout directory");
let session_meta_line = SessionMetaLine {
meta: SessionMeta {
account_scoped: false,
attached_thread: false,
creator_user_id: None,
creator_account_id: None,
+2
View File
@@ -1929,6 +1929,7 @@ async fn resolve_resume_thread_id(
ClientRequest::ThreadList {
request_id: RequestId::Integer(0),
params: ThreadListParams {
current_account_only: true,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -2017,6 +2018,7 @@ async fn resolve_resume_thread_id(
ClientRequest::ThreadList {
request_id: RequestId::Integer(0),
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
+4
View File
@@ -3416,6 +3416,9 @@ pub struct HistoryPosition {
/// and should be used when there is no config override.
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
pub struct SessionMeta {
/// Scope discovery to the creator. Absent on threads created before account filtering.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub account_scoped: bool,
/// ChatGPT user that created this thread; absent when unavailable or for older threads.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub creator_user_id: Option<String>,
@@ -3499,6 +3502,7 @@ impl Default for SessionMeta {
fn default() -> Self {
let id = ThreadId::default();
SessionMeta {
account_scoped: false,
creator_user_id: None,
creator_account_id: None,
session_id: id.into(),
+21 -1
View File
@@ -1,4 +1,5 @@
use codex_utils_absolute_path::test_support::PathExt;
use std::collections::HashMap;
use std::fs;
use std::fs::FileTimes;
#[cfg(unix)]
@@ -1070,13 +1071,31 @@ async fn find_thread_path_by_id_handles_compressed_rollout_filenames() -> anyhow
);
assert_eq!(
crate::find_rollout_path_by_rollout_id(home.path(), thread_id).await?,
Some(rollout_path)
Some(rollout_path.clone())
);
let missing_rollout_id = ThreadId::from_string(&Uuid::from_u128(2000).to_string())?;
assert_eq!(
crate::find_rollout_path_by_rollout_id(home.path(), missing_rollout_id).await?,
None
);
let archived_id = ThreadId::new();
let archived_path = home
.path()
.join(crate::ARCHIVED_SESSIONS_SUBDIR)
.join(format!("rollout-2025-01-03T12-00-00-{archived_id}.jsonl"));
write_rollout(&archived_path, archived_id, "archived original")?;
compress_now(&archived_path)?;
assert_eq!(
crate::find_rollout_paths_by_rollout_ids(
home.path(),
&[thread_id, archived_id, missing_rollout_id],
)
.await,
HashMap::from([
(thread_id, rollout_path),
(archived_id, compressed_rollout_path(&archived_path)),
])
);
assert_eq!(
crate::find_thread_path_by_id_str(home.path(), "not-a-uuid", /*state_db_ctx*/ None).await?,
None
@@ -1121,6 +1140,7 @@ fn write_rollout(path: &std::path::Path, thread_id: ThreadId, message: &str) ->
fs::create_dir_all(parent)?;
let session_meta_line = SessionMetaLine {
meta: SessionMeta {
account_scoped: false,
attached_thread: false,
creator_user_id: None,
creator_account_id: None,
+1
View File
@@ -123,6 +123,7 @@ pub use list::ThreadSortKey;
pub use list::ThreadsPage;
pub use list::find_archived_thread_path_by_id_str;
pub use list::find_rollout_path_by_rollout_id;
pub use list::find_rollout_paths_by_rollout_ids;
pub use list::find_thread_path_by_id_str;
#[deprecated(note = "use find_thread_path_by_id_str")]
pub use list::find_thread_path_by_id_str as find_conversation_path_by_id_str;
+35
View File
@@ -1579,6 +1579,41 @@ pub async fn find_rollout_path_by_rollout_id(
Ok(None)
}
/// Locate a page's rollout IDs with one filename traversal per storage root.
///
/// Keeps the first match, searching active storage before archives. On a read error, stops
/// scanning and retains earlier matches, just as individual lookups would have found them.
pub async fn find_rollout_paths_by_rollout_ids(
codex_home: &Path,
rollout_ids: &[RolloutId],
) -> HashMap<RolloutId, PathBuf> {
let ids: std::collections::HashSet<_> = rollout_ids.iter().copied().collect();
let mut paths = HashMap::new();
for subdir in [SESSIONS_SUBDIR, ARCHIVED_SESSIONS_SUBDIR] {
if paths.len() == ids.len() {
break;
}
if visit_rollout_filenames(
&codex_home.join(subdir),
|file_name| ids.contains(&file_name.rollout_id()),
|file_name, path| {
paths.entry(file_name.rollout_id()).or_insert(path);
if paths.len() == ids.len() {
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
},
)
.await
.is_err()
{
break;
}
}
paths
}
/// Prefetch a bounded set of active rollout paths for a history page.
///
/// The sibling directory supplies candidate IDs. They are resolved in the same traversal order as
+3
View File
@@ -117,6 +117,7 @@ async fn extract_metadata_from_rollout_uses_session_meta() {
.join(format!("rollout-2026-01-27T12-34-56-{uuid}.jsonl"));
let session_meta = SessionMeta {
account_scoped: false,
creator_user_id: Some("creator-user".to_string()),
attached_thread: false,
creator_account_id: Some("creator-account".to_string()),
@@ -242,6 +243,7 @@ async fn extract_metadata_from_rollout_returns_latest_memory_mode() {
.join(format!("rollout-2026-01-27T12-34-56-{uuid}.jsonl"));
let session_meta = SessionMeta {
account_scoped: false,
creator_user_id: None,
attached_thread: false,
creator_account_id: None,
@@ -592,6 +594,7 @@ fn write_rollout_in_sessions_with_cwd(
std::fs::create_dir_all(sessions_dir.as_path()).expect("create sessions dir");
let path = sessions_dir.join(format!("rollout-{filename_ts}-{thread_uuid}.jsonl"));
let session_meta = SessionMeta {
account_scoped: false,
creator_user_id: None,
attached_thread: false,
creator_account_id: None,
+12 -1
View File
@@ -106,6 +106,7 @@ pub enum RolloutRecorderParams {
source: Box<SessionSource>,
thread_source: Option<ThreadSource>,
originator: String,
account_scoped: bool,
creator_user_id: Option<String>,
creator_account_id: Option<String>,
base_instructions: BaseInstructions,
@@ -227,6 +228,7 @@ impl RolloutRecorderParams {
source: Box::new(source),
thread_source,
originator,
account_scoped: false,
creator_user_id: None,
creator_account_id: None,
base_instructions,
@@ -242,13 +244,20 @@ impl RolloutRecorderParams {
}
/// Record the authenticated identity at thread creation, or preserve it on revert.
pub fn with_creator(mut self, user_id: Option<String>, account_id: Option<String>) -> Self {
pub fn with_creator(
mut self,
user_id: Option<String>,
account_id: Option<String>,
account_scoped: bool,
) -> Self {
if let Self::Create {
creator_user_id,
creator_account_id,
account_scoped: scoped,
..
} = &mut self
{
*scoped = account_scoped;
*creator_user_id = user_id;
*creator_account_id = account_id;
}
@@ -920,6 +929,7 @@ impl RolloutRecorder {
source,
thread_source,
originator,
account_scoped,
creator_user_id,
creator_account_id,
base_instructions,
@@ -957,6 +967,7 @@ impl RolloutRecorder {
cwd: cwd.clone(),
runtime_workspace_roots,
originator,
account_scoped,
creator_user_id,
creator_account_id,
cli_version: env!("CARGO_PKG_VERSION").to_string(),
+1
View File
@@ -214,6 +214,7 @@ async fn state_db_init_backfills_before_returning() -> anyhow::Result<()> {
let session_meta_line = SessionMetaLine {
meta: SessionMeta {
account_scoped: false,
attached_thread: false,
creator_user_id: None,
creator_account_id: None,
@@ -35,6 +35,7 @@ fn write_rollout_with_source_and_provider(
ordinal: None,
item: RolloutItem::SessionMeta(Box::new(SessionMetaLine {
meta: SessionMeta {
account_scoped: false,
attached_thread: false,
creator_user_id: None,
creator_account_id: None,
+1
View File
@@ -325,6 +325,7 @@ fn write_rollout_with_user_message(
ordinal: None,
item: RolloutItem::SessionMeta(Box::new(SessionMetaLine {
meta: SessionMeta {
account_scoped: false,
attached_thread: false,
creator_user_id: None,
creator_account_id: None,
+1
View File
@@ -1593,6 +1593,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
ordinal: None,
item: RolloutItem::SessionMeta(Box::new(SessionMetaLine {
meta: SessionMeta {
account_scoped: false,
attached_thread: false,
creator_user_id: None,
creator_account_id: None,
+2
View File
@@ -534,6 +534,7 @@ mod tests {
&mut metadata,
&RolloutItem::SessionMeta(Box::new(SessionMetaLine {
meta: SessionMeta {
account_scoped: false,
creator_user_id: None,
attached_thread: false,
creator_account_id: None,
@@ -785,6 +786,7 @@ mod tests {
&mut metadata,
&RolloutItem::SessionMeta(Box::new(SessionMetaLine {
meta: SessionMeta {
account_scoped: false,
creator_user_id: None,
attached_thread: false,
creator_account_id: None,
+2
View File
@@ -2920,6 +2920,7 @@ mod tests {
);
let items = vec![RolloutItem::SessionMeta(Box::new(SessionMetaLine {
meta: SessionMeta {
account_scoped: false,
attached_thread: false,
creator_user_id: None,
creator_account_id: None,
@@ -2995,6 +2996,7 @@ mod tests {
);
let items = vec![RolloutItem::SessionMeta(Box::new(SessionMetaLine {
meta: SessionMeta {
account_scoped: false,
attached_thread: false,
creator_user_id: None,
creator_account_id: None,
+2
View File
@@ -8,6 +8,7 @@ mod error;
mod in_memory;
mod live_thread;
mod local;
mod local_history_account;
mod projects;
mod queue_store;
mod store;
@@ -42,6 +43,7 @@ pub use local::RolloutMigrationOutcome;
pub use local::RolloutMigrationProgress;
pub use local::RolloutMigrationReport;
pub use local::RolloutMigrationStatus;
pub use local_history_account::LocalHistoryAccount;
pub use projects::CreateProjectParams;
pub use projects::CreatedProject;
pub use projects::DeletedProject;
@@ -39,7 +39,11 @@ pub(super) async fn create_thread(
params.base_instructions,
params.dynamic_tools,
)
.with_creator(params.creator_user_id, params.creator_account_id)
.with_creator(
params.creator_user_id,
params.creator_account_id,
/*account_scoped*/ true,
)
.with_session_id(params.session_id)
.with_attached_thread(params.attached_thread)
.with_selected_capability_roots(params.selected_capability_roots)
@@ -179,7 +179,11 @@ async fn create_replacement_recorder(
source_meta.base_instructions.unwrap_or_default(),
source_meta.dynamic_tools.unwrap_or_default(),
)
.with_creator(source_meta.creator_user_id, source_meta.creator_account_id)
.with_creator(
source_meta.creator_user_id,
source_meta.creator_account_id,
source_meta.account_scoped,
)
.with_session_id(source_meta.session_id)
.with_attached_thread(source_meta.attached_thread)
.with_rollout_id(rollout_id)
@@ -19,6 +19,7 @@ use crate::ArchiveThreadParams;
use crate::CreateThreadParams;
use crate::DeleteThreadParams;
use crate::ListTurnsParams;
use crate::LocalHistoryAccount;
use crate::RevertThreadParams;
use crate::SortDirection;
use crate::StoredTurnItemsView;
@@ -96,10 +97,11 @@ async fn revert_keeps_thread_id_and_hides_suffix_across_repeated_reverts() {
assert_eq!(replacement_meta.id, thread_id);
assert_eq!(
(
replacement_meta.account_scoped,
replacement_meta.creator_user_id.as_deref(),
replacement_meta.creator_account_id.as_deref()
),
(Some("creator-user"), Some("creator-account")),
(true, Some("creator-user"), Some("creator-account")),
);
assert_eq!(replacement_meta.memory_mode, None);
assert_eq!(
@@ -117,6 +119,7 @@ async fn revert_keeps_thread_id_and_hides_suffix_across_repeated_reverts() {
let payload = meta_line["payload"]
.as_object_mut()
.expect("metadata payload");
payload.remove("account_scoped");
payload.remove("creator_user_id");
payload.remove("creator_account_id");
tokio::fs::write(
@@ -126,6 +129,25 @@ async fn revert_keeps_thread_id_and_hides_suffix_across_repeated_reverts() {
.await
.expect("write legacy replacement rollout");
for (user, expected) in [("creator-user", true), ("other-user", false)] {
let account = LocalHistoryAccount {
user_id: Some(user.to_string()),
account_id: Some("creator-account".to_string()),
};
assert_eq!(
account
.includes(
home.path(),
thread_id,
Some(&first_replacement_path),
&[thread_id],
/*original_paths*/ &mut None,
)
.await,
expected
);
}
store
.revert_thread(RevertThreadParams {
thread_id,
@@ -184,7 +206,7 @@ async fn revert_keeps_thread_id_and_hides_suffix_across_repeated_reverts() {
}
#[tokio::test]
async fn revert_preserves_resolved_multi_agent_version_in_session_metadata() {
async fn revert_preserves_legacy_scope_and_resolved_multi_agent_version() {
let home = TempDir::new().expect("temp dir");
let config = test_config(home.path());
let state_db = codex_state::StateRuntime::init(
@@ -194,6 +216,11 @@ async fn revert_preserves_resolved_multi_agent_version_in_session_metadata() {
.await
.expect("initialize state database");
let store = LocalThreadStore::new(config, Some(state_db.clone()));
let other_account = LocalHistoryAccount {
user_id: Some("other-user".to_string()),
account_id: Some("other-account".to_string()),
};
let mut archived_threads = Vec::new();
for version in [
MultiAgentVersion::V1,
@@ -222,6 +249,17 @@ async fn revert_preserves_resolved_multi_agent_version_in_session_metadata() {
.shutdown_thread(thread_id)
.await
.expect("close source writer");
let contents = tokio::fs::read_to_string(&path)
.await
.expect("read rollout");
// Preserve indexed offsets while turning the fixture into pre-enforcement history.
let marker = "\"account_scoped\":true,";
tokio::fs::write(
&path,
contents.replacen(marker, &" ".repeat(marker.len()), 1),
)
.await
.expect("write legacy rollout");
codex_rollout::state_db::reconcile_rollout(
Some(state_db.as_ref()),
path.as_path(),
@@ -252,8 +290,48 @@ async fn revert_preserves_resolved_multi_agent_version_in_session_metadata() {
let meta = codex_rollout::read_session_meta_line(&path)
.await
.expect("read replacement metadata");
assert_eq!(meta.meta.multi_agent_version, Some(version));
assert_eq!(
(meta.meta.multi_agent_version, meta.meta.account_scoped),
(Some(version), false)
);
assert!(
other_account
.includes(
home.path(),
thread_id,
Some(&path),
&[thread_id],
/*original_paths*/ &mut None,
)
.await
);
}
store
.archive_thread(ArchiveThreadParams { thread_id })
.await
.expect("archive reverted legacy thread");
let path = state_db
.get_thread(thread_id)
.await
.expect("read archived metadata")
.expect("archived thread")
.rollout_path;
archived_threads.push((thread_id, path));
}
let page_thread_ids: Vec<_> = archived_threads.iter().map(|(id, _)| *id).collect();
let mut original_paths = None;
for (thread_id, path) in archived_threads {
assert!(
other_account
.includes(
home.path(),
thread_id,
Some(&path),
&page_thread_ids,
&mut original_paths,
)
.await
);
}
}
@@ -125,19 +125,42 @@ pub(super) async fn search_threads(
sort_direction,
)
.await?;
let page_thread_ids: Vec<_> = page
.items
.iter()
.filter_map(|item| item.thread_id)
.collect();
let mut original_paths = None;
for item in page.items {
let logical_path = rollout_search_path(item.path.as_path());
let Some(snippet) = (match remaining_rollouts.remove(logical_path.as_path()) {
Some(Some(snippet)) => Some(snippet),
Some(None) => first_rollout_content_match_snippet(item.path.as_path(), search_term)
let Some(mut snippet) = remaining_rollouts.remove(logical_path.as_path()) else {
continue;
};
if let Some(account) = &params.account {
let Some(thread_id) = item.thread_id else {
continue;
};
if !account
.includes(
&store.config.codex_home,
thread_id,
Some(&item.path),
&page_thread_ids,
&mut original_paths,
)
.await
{
continue;
}
}
if snippet.is_none() {
snippet = first_rollout_content_match_snippet(item.path.as_path(), search_term)
.await
.map_err(|err| ThreadStoreError::Internal {
message: format!("failed to read rollout search match: {err}"),
})?,
None => None,
}) else {
continue;
};
})?;
}
let Some(snippet) = snippet else { continue };
matching_items.push(ThreadSearchItem { item, snippet });
if matching_items.len() > params.page_size {
break;
@@ -141,6 +141,7 @@ async fn search_matches_selected_rollout_across_path_spellings_and_compression()
] {
let page = store
.search_threads(SearchThreadsParams {
account: None,
page_size: 10,
cursor: None,
sort_key: ThreadSortKey::CreatedAt,
@@ -0,0 +1,67 @@
//! Account matching for local discovery. Pre-enforcement threads remain shared.
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use codex_protocol::ThreadId;
use serde::Deserialize;
use serde::Serialize;
/// The current ChatGPT identity for account-scoped local history discovery.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LocalHistoryAccount {
pub user_id: Option<String>,
pub account_id: Option<String>,
}
impl LocalHistoryAccount {
/// Newly scoped threads must match each creator ID; older history stays shared.
/// Reuse `original_paths` only within the page identified by `page_thread_ids`.
pub async fn includes(
&self,
codex_home: &Path,
thread_id: ThreadId,
rollout_path: Option<&Path>,
page_thread_ids: &[ThreadId],
original_paths: &mut Option<HashMap<ThreadId, PathBuf>>,
) -> bool {
let Some(path) = rollout_path else {
return false;
};
let Ok(header) = codex_rollout::read_session_meta_line(path).await else {
return false;
};
let mut meta = header.meta;
if meta.id != thread_id {
return false;
}
if !meta.account_scoped
&& codex_rollout::rollout_id_from_path(path).is_some_and(|id| id != thread_id)
{
// An older binary can drop the marker on revert; consult the original rollout.
// Resolve the page together so each reverted thread does not rescan all history.
if original_paths.is_none() {
*original_paths = Some(
codex_rollout::find_rollout_paths_by_rollout_ids(codex_home, page_thread_ids)
.await,
);
}
let Some(original) = original_paths
.as_ref()
.and_then(|paths| paths.get(&thread_id))
else {
return false;
};
let Ok(header) = codex_rollout::read_session_meta_line(original).await else {
return false;
};
meta = header.meta;
}
meta.id == thread_id
&& (!meta.account_scoped
|| ((meta.creator_user_id.is_none() || meta.creator_user_id == self.user_id)
&& (meta.creator_account_id.is_none()
|| meta.creator_account_id == self.account_id)))
}
}
+3
View File
@@ -360,6 +360,9 @@ pub struct ListThreadsParams {
/// Parameters for searching thread content.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SearchThreadsParams {
/// The current login when searching account-scoped local history.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account: Option<crate::LocalHistoryAccount>,
/// Maximum number of threads to return.
pub page_size: usize,
/// Opaque cursor returned by a previous search call.
+1
View File
@@ -144,6 +144,7 @@ pub(super) async fn roots(
pub(super) fn list_params() -> ThreadListParams {
ThreadListParams {
current_account_only: true,
excluded_thread_ids: None,
attached_to_thread_id: None,
limit: Some(100),
+1
View File
@@ -190,6 +190,7 @@ async fn descendants(
.request_typed(ClientRequest::ThreadList {
request_id: RequestId::String(uuid::Uuid::new_v4().to_string()),
params: ThreadListParams {
current_account_only: false,
ancestor_thread_id: Some(root.id.clone()),
archived: Some(archived),
cursor,
+1
View File
@@ -40,6 +40,7 @@ impl App {
.request_typed::<ThreadListResponse>(ClientRequest::ThreadList {
request_id: RequestId::String(Uuid::new_v4().to_string()),
params: ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -44,6 +44,7 @@ pub(crate) async fn list_pinned_threads(
.request_typed::<ThreadListResponse>(ClientRequest::ThreadList {
request_id: RequestId::String(Uuid::new_v4().to_string()),
params: ThreadListParams {
current_account_only: true,
excluded_thread_ids: None,
originators: None,
cursor: cursor.clone(),
@@ -186,6 +187,7 @@ impl SourcePage {
.request_typed::<ThreadListResponse>(ClientRequest::ThreadList {
request_id: RequestId::String(Uuid::new_v4().to_string()),
params: ThreadListParams {
current_account_only: true,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
+1
View File
@@ -351,6 +351,7 @@ async fn execute_inner(
request(&handle, |request_id| ClientRequest::ThreadList {
request_id,
params: ThreadListParams {
current_account_only: true,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
+1
View File
@@ -932,6 +932,7 @@ fn latest_session_lookup_params(
lookup_mode: LatestSessionLookupMode,
) -> ThreadListParams {
ThreadListParams {
current_account_only: true,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
+1
View File
@@ -64,6 +64,7 @@ pub(super) async fn lookup(
loop {
let response = app_server
.thread_list(ThreadListParams {
current_account_only: false,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
+1
View File
@@ -2515,6 +2515,7 @@ fn thread_list_params(
use_state_db_only: bool,
) -> ThreadListParams {
ThreadListParams {
current_account_only: true,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
+2
View File
@@ -73,6 +73,7 @@ pub(crate) fn spawn_search(
handle.request_typed::<ThreadSearchResponse>(ClientRequest::ThreadSearch {
request_id: RequestId::String(format!("task-search-{request_generation}")),
params: ThreadSearchParams {
current_account_only: true,
cursor: None,
limit: Some(MAX_SEARCH_RESULTS),
sort_key: Some(ThreadSearchSortKey::RecencyAt),
@@ -90,6 +91,7 @@ pub(crate) fn spawn_search(
"task-title-search-{request_generation}"
)),
params: ThreadListParams {
current_account_only: true,
excluded_thread_ids: None,
attached_to_thread_id: None,
originators: None,
@@ -10857,6 +10857,13 @@ class ThreadListParams(BaseModel):
description="Optional archived filter; when set to true, only archived threads are returned. If false or null, only non-archived threads are returned."
),
] = None
current_account_only: Annotated[
bool | None,
Field(
alias="currentAccountOnly",
description="Show local history visible to the current ChatGPT account. Defaults to false.",
),
] = None
cursor: Annotated[
str | None, Field(description="Opaque pagination cursor returned by a previous call.")
] = None