mirror of
https://github.com/openai/codex.git
synced 2026-11-12 14:41:42 +00:00
Expose thread originators through the app-server API (#42458)
## What changed - Add the creation-time `originator` to app-server thread responses and `thread/started` notifications. - Persist originators in thread metadata and preserve the first recorded value across list, read, resume, rollout, and SQLite paths. - Add an `originators` allowlist to `thread/list` for hosted backends. The local app-server accepts omitted or empty values but rejects nonempty filters. ## Testing - Cover originator persistence across server restarts and list, read, and resume requests. - Verify local `thread/list` handling for nonempty, omitted, null, and empty originator filters. - Heap-allocate large TUI dispatcher futures in affected tests to keep them within the Windows test-thread stack. GitOrigin-RevId: 73ad6e61b8fdd2d93a22d1f971723a19ec7be3fc
This commit is contained in:
1 parent
8ff74cc9b1
commit
728cb12fe5
75 files changed
+511
-12
No files matched your search
@@ -242,6 +242,7 @@ fn sample_thread_with_metadata(
|
|||||||
parent_thread_id: Option<String>,
|
parent_thread_id: Option<String>,
|
||||||
) -> Thread {
|
) -> Thread {
|
||||||
Thread {
|
Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -589,6 +589,7 @@ fn sample_thread_archive_request() -> ClientRequest {
|
|||||||
|
|
||||||
fn sample_thread(thread_id: &str) -> Thread {
|
fn sample_thread(thread_id: &str) -> Thread {
|
||||||
Thread {
|
Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -4448,6 +4448,16 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originators": {
|
||||||
|
"description": "Optional originator allowlist, matching any supplied value exactly. Supported by hosted backends only; the local app-server rejects a nonempty list. Omitted or empty lists leave originators unrestricted.",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"searchTerm": {
|
"searchTerm": {
|
||||||
"description": "Optional substring filter for the extracted thread title.",
|
"description": "Optional substring filter for the extracted thread title.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
@@ -4172,6 +4172,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
+17
@@ -20339,6 +20339,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
@@ -22030,6 +22037,16 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originators": {
|
||||||
|
"description": "Optional originator allowlist, matching any supplied value exactly. Supported by hosted backends only; the local app-server rejects a nonempty list. Omitted or empty lists leave originators unrestricted.",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"searchTerm": {
|
"searchTerm": {
|
||||||
"description": "Optional substring filter for the extracted thread title.",
|
"description": "Optional substring filter for the extracted thread title.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
+17
@@ -18053,6 +18053,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
@@ -19744,6 +19751,16 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originators": {
|
||||||
|
"description": "Optional originator allowlist, matching any supplied value exactly. Supported by hosted backends only; the local app-server rejects a nonempty list. Omitted or empty lists leave originators unrestricted.",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"searchTerm": {
|
"searchTerm": {
|
||||||
"description": "Optional substring filter for the extracted thread title.",
|
"description": "Optional substring filter for the extracted thread title.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
@@ -1352,6 +1352,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
@@ -91,6 +91,16 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originators": {
|
||||||
|
"description": "Optional originator allowlist, matching any supplied value exactly. Supported by hosted backends only; the local app-server rejects a nonempty list. Omitted or empty lists leave originators unrestricted.",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"searchTerm": {
|
"searchTerm": {
|
||||||
"description": "Optional substring filter for the extracted thread title.",
|
"description": "Optional substring filter for the extracted thread title.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
@@ -1143,6 +1143,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
+7
@@ -1143,6 +1143,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
@@ -1143,6 +1143,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
@@ -1352,6 +1352,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
@@ -1143,6 +1143,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
@@ -1143,6 +1143,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
@@ -1352,6 +1352,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
@@ -1143,6 +1143,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
@@ -1143,6 +1143,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"originator": {
|
||||||
|
"description": "Originator recorded when the thread was created, independent of its current client or executor. Null when the recorded originator is unavailable.",
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"parentThreadId": {
|
"parentThreadId": {
|
||||||
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
"description": "The ID of the parent thread. This will only be set if this thread is a subagent.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -74,6 +74,10 @@ cwd: AbsolutePathBuf, /**
|
|||||||
* Version of the CLI that created the thread.
|
* Version of the CLI that created the thread.
|
||||||
*/
|
*/
|
||||||
cliVersion: string, /**
|
cliVersion: string, /**
|
||||||
|
* Originator recorded when the thread was created, independent of its current client or executor.
|
||||||
|
* Null when the recorded originator is unavailable.
|
||||||
|
*/
|
||||||
|
originator: string | null, /**
|
||||||
* Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).
|
* Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).
|
||||||
*/
|
*/
|
||||||
source: SessionSource, /**
|
source: SessionSource, /**
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ modelProviders?: Array<string> | null, /**
|
|||||||
* are returned. When omitted or empty, defaults to interactive sources.
|
* are returned. When omitted or empty, defaults to interactive sources.
|
||||||
*/
|
*/
|
||||||
sourceKinds?: Array<ThreadSourceKind> | null, /**
|
sourceKinds?: Array<ThreadSourceKind> | null, /**
|
||||||
|
* Optional originator allowlist, matching any supplied value exactly.
|
||||||
|
* Supported by hosted backends only; the local app-server rejects a nonempty list.
|
||||||
|
* Omitted or empty lists leave originators unrestricted.
|
||||||
|
*/
|
||||||
|
originators?: Array<string> | null, /**
|
||||||
* Optional archived filter; when set to true, only archived threads are returned.
|
* Optional archived filter; when set to true, only archived threads are returned.
|
||||||
* If false or null, only non-archived threads are returned.
|
* If false or null, only non-archived threads are returned.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3128,6 +3128,7 @@ mod tests {
|
|||||||
request_id: RequestId::Integer(7),
|
request_id: RequestId::Integer(7),
|
||||||
response: v2::ThreadStartResponse {
|
response: v2::ThreadStartResponse {
|
||||||
thread: v2::Thread {
|
thread: v2::Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
@@ -3210,6 +3211,7 @@ mod tests {
|
|||||||
"path": null,
|
"path": null,
|
||||||
"cwd": absolute_path_string("tmp"),
|
"cwd": absolute_path_string("tmp"),
|
||||||
"cliVersion": "0.0.0",
|
"cliVersion": "0.0.0",
|
||||||
|
"originator": null,
|
||||||
"source": "exec",
|
"source": "exec",
|
||||||
"canAcceptDirectInput": null,
|
"canAcceptDirectInput": null,
|
||||||
"threadSource": null,
|
"threadSource": null,
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ fn thread_resume_params_accept_turns_page_bootstrap() {
|
|||||||
fn thread_resume_response_round_trips_initial_turns_page() {
|
fn thread_resume_response_round_trips_initial_turns_page() {
|
||||||
let response = ThreadResumeResponse {
|
let response = ThreadResumeResponse {
|
||||||
thread: Thread {
|
thread: Thread {
|
||||||
|
originator: Some("future_client".to_string()),
|
||||||
environments: Some(vec![ThreadEnvironment {
|
environments: Some(vec![ThreadEnvironment {
|
||||||
environment_id: "remote".to_string(),
|
environment_id: "remote".to_string(),
|
||||||
cwd: LegacyAppPathString::from_string(r"C:\workspace"),
|
cwd: LegacyAppPathString::from_string(r"C:\workspace"),
|
||||||
@@ -335,6 +336,7 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let value = serde_json::to_value(&response).expect("serialize thread resume response");
|
let value = serde_json::to_value(&response).expect("serialize thread resume response");
|
||||||
|
assert_eq!(value["thread"]["originator"], json!("future_client"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value["thread"]["environments"],
|
value["thread"]["environments"],
|
||||||
json!([{
|
json!([{
|
||||||
@@ -359,12 +361,14 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
|||||||
legacy_thread_fields.remove("sectionEnteredAt");
|
legacy_thread_fields.remove("sectionEnteredAt");
|
||||||
legacy_thread_fields.remove("projectId");
|
legacy_thread_fields.remove("projectId");
|
||||||
legacy_thread_fields.remove("environments");
|
legacy_thread_fields.remove("environments");
|
||||||
|
legacy_thread_fields.remove("originator");
|
||||||
let legacy_thread =
|
let legacy_thread =
|
||||||
serde_json::from_value::<Thread>(legacy_thread).expect("deserialize legacy thread");
|
serde_json::from_value::<Thread>(legacy_thread).expect("deserialize legacy thread");
|
||||||
assert_eq!(legacy_thread.section, None);
|
assert_eq!(legacy_thread.section, None);
|
||||||
assert_eq!(legacy_thread.section_entered_at, None);
|
assert_eq!(legacy_thread.section_entered_at, None);
|
||||||
assert_eq!(legacy_thread.project_id, None);
|
assert_eq!(legacy_thread.project_id, None);
|
||||||
assert_eq!(legacy_thread.environments, None);
|
assert_eq!(legacy_thread.environments, None);
|
||||||
|
assert_eq!(legacy_thread.originator, None);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value.get("initialTurnsPage"),
|
value.get("initialTurnsPage"),
|
||||||
|
|||||||
@@ -1386,6 +1386,11 @@ pub struct ThreadListParams {
|
|||||||
/// are returned. When omitted or empty, defaults to interactive sources.
|
/// are returned. When omitted or empty, defaults to interactive sources.
|
||||||
#[ts(optional = nullable)]
|
#[ts(optional = nullable)]
|
||||||
pub source_kinds: Option<Vec<ThreadSourceKind>>,
|
pub source_kinds: Option<Vec<ThreadSourceKind>>,
|
||||||
|
/// Optional originator allowlist, matching any supplied value exactly.
|
||||||
|
/// Supported by hosted backends only; the local app-server rejects a nonempty list.
|
||||||
|
/// Omitted or empty lists leave originators unrestricted.
|
||||||
|
#[ts(optional = nullable)]
|
||||||
|
pub originators: Option<Vec<String>>,
|
||||||
/// Optional archived filter; when set to true, only archived threads are returned.
|
/// Optional archived filter; when set to true, only archived threads are returned.
|
||||||
/// If false or null, only non-archived threads are returned.
|
/// If false or null, only non-archived threads are returned.
|
||||||
#[ts(optional = nullable)]
|
#[ts(optional = nullable)]
|
||||||
|
|||||||
@@ -264,6 +264,9 @@ pub struct Thread {
|
|||||||
pub cwd: AbsolutePathBuf,
|
pub cwd: AbsolutePathBuf,
|
||||||
/// Version of the CLI that created the thread.
|
/// Version of the CLI that created the thread.
|
||||||
pub cli_version: String,
|
pub cli_version: String,
|
||||||
|
/// Originator recorded when the thread was created, independent of its current client or executor.
|
||||||
|
/// Null when the recorded originator is unavailable.
|
||||||
|
pub originator: Option<String>,
|
||||||
/// Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).
|
/// Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).
|
||||||
pub source: SessionSource,
|
pub source: SessionSource,
|
||||||
/// Whether the app server accepts direct turn input for this loaded thread.
|
/// Whether the app server accepts direct turn input for this loaded thread.
|
||||||
@@ -319,6 +322,7 @@ struct ThreadCompatibility {
|
|||||||
path: Option<PathBuf>,
|
path: Option<PathBuf>,
|
||||||
cwd: AbsolutePathBuf,
|
cwd: AbsolutePathBuf,
|
||||||
cli_version: String,
|
cli_version: String,
|
||||||
|
originator: Option<String>,
|
||||||
source: SessionSource,
|
source: SessionSource,
|
||||||
can_accept_direct_input: Option<bool>,
|
can_accept_direct_input: Option<bool>,
|
||||||
thread_source: Option<ThreadSource>,
|
thread_source: Option<ThreadSource>,
|
||||||
@@ -358,6 +362,7 @@ impl<'de> Deserialize<'de> for Thread {
|
|||||||
path: thread.path,
|
path: thread.path,
|
||||||
cwd: thread.cwd,
|
cwd: thread.cwd,
|
||||||
cli_version: thread.cli_version,
|
cli_version: thread.cli_version,
|
||||||
|
originator: thread.originator,
|
||||||
source: thread.source,
|
source: thread.source,
|
||||||
can_accept_direct_input: thread.can_accept_direct_input,
|
can_accept_direct_input: thread.can_accept_direct_input,
|
||||||
thread_source: thread.thread_source,
|
thread_source: thread.thread_source,
|
||||||
|
|||||||
@@ -1301,6 +1301,7 @@ async fn thread_list(endpoint: &Endpoint, config_overrides: &[String], limit: u3
|
|||||||
println!("< initialize response: {initialize:?}");
|
println!("< initialize response: {initialize:?}");
|
||||||
|
|
||||||
let response = client.thread_list(ThreadListParams {
|
let response = client.thread_list(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(limit),
|
limit: Some(limit),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
|
|||||||
@@ -563,6 +563,7 @@ Pass any combination of:
|
|||||||
- `sortDirection` — `desc` (default for timestamp sorts) or `asc` (default for `section_position`).
|
- `sortDirection` — `desc` (default for timestamp sorts) or `asc` (default for `section_position`).
|
||||||
- `modelProviders` — restrict results to specific providers; unset, null, or an empty array will include all providers.
|
- `modelProviders` — restrict results to specific providers; unset, null, or an empty array will include all providers.
|
||||||
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
|
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
|
||||||
|
- `originators` — an exact-value allowlist for hosted backends that support originator filtering. The local app-server rejects a nonempty list; omission, `null`, and `[]` leave originators unrestricted.
|
||||||
- `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default).
|
- `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default).
|
||||||
- `sectionId` — provide an ID from `threadSection/list` to return threads from that section; pass `null` to return only threads without a section; or omit it to include threads from every section and threads without a section.
|
- `sectionId` — provide an ID from `threadSection/list` to return threads from that section; pass `null` to return only threads without a section; or omit it to include threads from every section and threads without a section.
|
||||||
- `cwd` — restrict results to threads whose session cwd exactly matches this path, or one of these paths when an array is provided. Relative paths are resolved against the app-server process cwd before matching.
|
- `cwd` — restrict results to threads whose session cwd exactly matches this path, or one of these paths when an array is provided. Relative paths are resolved against the app-server process cwd before matching.
|
||||||
@@ -570,6 +571,7 @@ Pass any combination of:
|
|||||||
- `searchTerm` — restrict results to threads whose extracted title contains this substring (case-sensitive).
|
- `searchTerm` — restrict results to threads whose extracted title contains this substring (case-sensitive).
|
||||||
- Responses include `nextCursor` to continue in the same direction and `backwardsCursor` to pass as `cursor` when reversing `sortDirection`.
|
- Responses include `nextCursor` to continue in the same direction and `backwardsCursor` to pass as `cursor` when reversing `sortDirection`.
|
||||||
- Responses include `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available.
|
- Responses include `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available.
|
||||||
|
- Full thread responses and `thread/started` include `originator`, the value recorded at creation, or `null` when unavailable. Opening or resuming a thread does not attribute it to the current client. This is separate from the runtime `source` and does not choose an executor.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|||||||
@@ -2297,6 +2297,7 @@ mod tests {
|
|||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
let stored_thread = StoredThread {
|
let stored_thread = StoredThread {
|
||||||
|
originator: None,
|
||||||
thread_id,
|
thread_id,
|
||||||
extra_config: None,
|
extra_config: None,
|
||||||
rollout_path: None,
|
rollout_path: None,
|
||||||
|
|||||||
@@ -2513,6 +2513,7 @@ impl ThreadRequestProcessor {
|
|||||||
sort_direction,
|
sort_direction,
|
||||||
model_providers,
|
model_providers,
|
||||||
source_kinds,
|
source_kinds,
|
||||||
|
originators,
|
||||||
archived,
|
archived,
|
||||||
section_id,
|
section_id,
|
||||||
project_id,
|
project_id,
|
||||||
@@ -2522,6 +2523,14 @@ impl ThreadRequestProcessor {
|
|||||||
parent_thread_id,
|
parent_thread_id,
|
||||||
ancestor_thread_id,
|
ancestor_thread_id,
|
||||||
} = params;
|
} = params;
|
||||||
|
if originators
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|values| !values.is_empty())
|
||||||
|
{
|
||||||
|
return Err(invalid_params(
|
||||||
|
"originator filtering is not supported by the local app-server",
|
||||||
|
));
|
||||||
|
}
|
||||||
if project_id.is_some() && !self.thread_store.supports_projects() {
|
if project_id.is_some() && !self.thread_store.supports_projects() {
|
||||||
return Err(unsupported_thread_store_operation("projects"));
|
return Err(unsupported_thread_store_operation("projects"));
|
||||||
}
|
}
|
||||||
@@ -5948,6 +5957,7 @@ pub(crate) fn thread_from_stored_thread(
|
|||||||
path,
|
path,
|
||||||
cwd,
|
cwd,
|
||||||
cli_version: thread.cli_version,
|
cli_version: thread.cli_version,
|
||||||
|
originator: thread.originator,
|
||||||
agent_nickname: source.get_nickname(),
|
agent_nickname: source.get_nickname(),
|
||||||
agent_role: source.get_agent_role(),
|
agent_role: source.get_agent_role(),
|
||||||
source: source.into(),
|
source: source.into(),
|
||||||
@@ -6128,6 +6138,8 @@ fn build_thread_from_snapshot(
|
|||||||
path,
|
path,
|
||||||
cwd: config_snapshot.cwd().clone(),
|
cwd: config_snapshot.cwd().clone(),
|
||||||
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
|
originator: (!config_snapshot.originator.is_empty())
|
||||||
|
.then(|| config_snapshot.originator.clone()),
|
||||||
agent_nickname: config_snapshot.session_source.get_nickname(),
|
agent_nickname: config_snapshot.session_source.get_nickname(),
|
||||||
agent_role: config_snapshot.session_source.get_agent_role(),
|
agent_role: config_snapshot.session_source.get_agent_role(),
|
||||||
source: config_snapshot.session_source.clone().into(),
|
source: config_snapshot.session_source.clone().into(),
|
||||||
|
|||||||
@@ -459,6 +459,7 @@ mod thread_processor_behavior_tests {
|
|||||||
let thread_id =
|
let thread_id =
|
||||||
ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread");
|
ThreadId::from_string("00000000-0000-0000-0000-000000000123").expect("valid thread");
|
||||||
let stored_thread = StoredThread {
|
let stored_thread = StoredThread {
|
||||||
|
originator: None,
|
||||||
thread_id,
|
thread_id,
|
||||||
extra_config: None,
|
extra_config: None,
|
||||||
rollout_path: Some(PathBuf::from("/tmp/thread.jsonl")),
|
rollout_path: Some(PathBuf::from("/tmp/thread.jsonl")),
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ mod tests {
|
|||||||
|
|
||||||
fn test_thread(items: Vec<ThreadItem>) -> Thread {
|
fn test_thread(items: Vec<ThreadItem>) -> Thread {
|
||||||
Thread {
|
Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: "thread-1".to_string(),
|
id: "thread-1".to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ pub(crate) fn summary_to_thread(
|
|||||||
|
|
||||||
let thread_id = conversation_id.to_string();
|
let thread_id = conversation_id.to_string();
|
||||||
Thread {
|
Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: thread_id.clone(),
|
id: thread_id.clone(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -606,6 +606,7 @@ source = {:?}
|
|||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -1970,6 +1971,7 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
|||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -2160,6 +2162,7 @@ required = true
|
|||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -2245,6 +2248,7 @@ async fn external_agent_config_import_accepts_detected_session_payload_after_res
|
|||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -2327,6 +2331,7 @@ async fn external_agent_config_import_skips_already_imported_session_versions()
|
|||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -2455,6 +2460,7 @@ async fn external_agent_config_import_returns_before_background_session_import_f
|
|||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -2571,6 +2577,7 @@ async fn external_agent_config_import_compacts_huge_session_before_first_follow_
|
|||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ async fn projects_list_by_recency_and_preserve_metadata_timestamps() -> Result<(
|
|||||||
.request(|request_id| ClientRequest::ThreadList {
|
.request(|request_id| ClientRequest::ThreadList {
|
||||||
request_id,
|
request_id,
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -323,6 +324,7 @@ async fn projects_persist_and_assign_threads() -> Result<()> {
|
|||||||
.request(|request_id| ClientRequest::ThreadList {
|
.request(|request_id| ClientRequest::ThreadList {
|
||||||
request_id,
|
request_id,
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -368,6 +370,7 @@ async fn projects_persist_and_assign_threads() -> Result<()> {
|
|||||||
.request(|request_id| ClientRequest::ThreadList {
|
.request(|request_id| ClientRequest::ThreadList {
|
||||||
request_id,
|
request_id,
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -416,6 +419,7 @@ async fn projects_persist_and_assign_threads() -> Result<()> {
|
|||||||
.request(|request_id| ClientRequest::ThreadList {
|
.request(|request_id| ClientRequest::ThreadList {
|
||||||
request_id,
|
request_id,
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -609,6 +613,7 @@ async fn projects_persist_and_assign_threads() -> Result<()> {
|
|||||||
.request(|request_id| ClientRequest::ThreadList {
|
.request(|request_id| ClientRequest::ThreadList {
|
||||||
request_id,
|
request_id,
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -699,6 +704,7 @@ async fn deleted_project_is_dropped_before_first_durable_thread_persistence() ->
|
|||||||
.request(|request_id| ClientRequest::ThreadList {
|
.request(|request_id| ClientRequest::ThreadList {
|
||||||
request_id,
|
request_id,
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -888,6 +894,7 @@ async fn projects_validate_filters_cursors_and_sqlite_less_assignment() -> Resul
|
|||||||
for project_id in [String::new(), Uuid::now_v7().to_string()] {
|
for project_id in [String::new(), Uuid::now_v7().to_string()] {
|
||||||
let request_id = server
|
let request_id = server
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -972,6 +979,7 @@ async fn projects_validate_filters_cursors_and_sqlite_less_assignment() -> Resul
|
|||||||
assert_eq!(error.error.code, -32601);
|
assert_eq!(error.error.code, -32601);
|
||||||
let list_id = unsupported_projects
|
let list_id = unsupported_projects
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ async fn thread_delete_with_non_local_thread_store_does_not_create_local_persist
|
|||||||
.request(ClientRequest::ThreadList {
|
.request(ClientRequest::ThreadList {
|
||||||
request_id: RequestId::Integer(3),
|
request_id: RequestId::Integer(3),
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs
|
|||||||
async fn list_threads(mcp: &mut TestAppServer) -> Result<ThreadListResponse> {
|
async fn list_threads(mcp: &mut TestAppServer) -> Result<ThreadListResponse> {
|
||||||
let list_id = mcp
|
let list_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(50),
|
limit: Some(50),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ async fn list_threads_with_sort(
|
|||||||
mcp.request(|request_id| ClientRequest::ThreadList {
|
mcp.request(|request_id| ClientRequest::ThreadList {
|
||||||
request_id,
|
request_id,
|
||||||
params: codex_app_server_protocol::ThreadListParams {
|
params: codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor,
|
cursor,
|
||||||
limit,
|
limit,
|
||||||
sort_key,
|
sort_key,
|
||||||
@@ -141,6 +142,7 @@ async fn list_threads_for_relation(
|
|||||||
mcp.request(|request_id| ClientRequest::ThreadList {
|
mcp.request(|request_id| ClientRequest::ThreadList {
|
||||||
request_id,
|
request_id,
|
||||||
params: codex_app_server_protocol::ThreadListParams {
|
params: codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor,
|
cursor,
|
||||||
limit: Some(limit),
|
limit: Some(limit),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -546,6 +548,7 @@ async fn thread_list_respects_cwd_filters() -> Result<()> {
|
|||||||
let mut mcp = init_mcp(codex_home.path()).await?;
|
let mut mcp = init_mcp(codex_home.path()).await?;
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -659,6 +662,7 @@ sqlite = true
|
|||||||
let mut mcp = init_mcp(codex_home.path()).await?;
|
let mut mcp = init_mcp(codex_home.path()).await?;
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -946,6 +950,7 @@ sqlite = true
|
|||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -982,6 +987,7 @@ sqlite = true
|
|||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -1011,6 +1017,7 @@ sqlite = true
|
|||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -1196,6 +1203,7 @@ async fn thread_list_relation_filters_reject_invalid_requests() -> Result<()> {
|
|||||||
let mut mcp = init_mcp(codex_home.path()).await?;
|
let mut mcp = init_mcp(codex_home.path()).await?;
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -1222,6 +1230,7 @@ async fn thread_list_relation_filters_reject_invalid_requests() -> Result<()> {
|
|||||||
let thread_id = ThreadId::new().to_string();
|
let thread_id = ThreadId::new().to_string();
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -1508,6 +1517,7 @@ async fn thread_list_reports_loaded_subagent_direct_input_capability() -> Result
|
|||||||
.request(|request_id| ClientRequest::ThreadList {
|
.request(|request_id| ClientRequest::ThreadList {
|
||||||
request_id,
|
request_id,
|
||||||
params: codex_app_server_protocol::ThreadListParams {
|
params: codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -2329,6 +2339,7 @@ async fn thread_list_backwards_cursor_can_seed_forward_delta_sync() -> Result<()
|
|||||||
} = {
|
} = {
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(1),
|
limit: Some(1),
|
||||||
sort_key: Some(ThreadSortKey::UpdatedAt),
|
sort_key: Some(ThreadSortKey::UpdatedAt),
|
||||||
@@ -2370,6 +2381,7 @@ async fn thread_list_backwards_cursor_can_seed_forward_delta_sync() -> Result<()
|
|||||||
} = {
|
} = {
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: Some(backwards_cursor),
|
cursor: Some(backwards_cursor),
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: Some(ThreadSortKey::UpdatedAt),
|
sort_key: Some(ThreadSortKey::UpdatedAt),
|
||||||
@@ -2598,6 +2610,138 @@ async fn thread_list_archived_filter() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_list_rejects_originator_filter_but_accepts_empty_allowlist() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
create_minimal_config(codex_home.path())?;
|
||||||
|
let mut mcp = init_mcp(codex_home.path()).await?;
|
||||||
|
let request_id = mcp
|
||||||
|
.send_thread_list_request(serde_json::from_value(json!({
|
||||||
|
"originators": ["future_client"]
|
||||||
|
}))?)
|
||||||
|
.await?;
|
||||||
|
let error = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
assert_eq!(
|
||||||
|
(error.error.code, error.error.message),
|
||||||
|
(
|
||||||
|
-32602,
|
||||||
|
"originator filtering is not supported by the local app-server".to_string()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for params in [
|
||||||
|
json!({}),
|
||||||
|
json!({"originators": null}),
|
||||||
|
json!({"originators": []}),
|
||||||
|
] {
|
||||||
|
let response: ThreadListResponse = mcp
|
||||||
|
.request(|request_id| ClientRequest::ThreadList {
|
||||||
|
request_id,
|
||||||
|
params: serde_json::from_value(params).expect("valid list params"),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
assert_eq!(response.data, Vec::new());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case::test_case("codex_work_desktop")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_originator_is_preserved_in_list_read_and_resume(originator: &str) -> Result<()> {
|
||||||
|
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
create_runtime_config(codex_home.path(), &server.uri())?;
|
||||||
|
let mut mcp = init_mcp(codex_home.path()).await?;
|
||||||
|
let ThreadStartResponse { thread, .. } = mcp
|
||||||
|
.start_thread(ThreadStartParams {
|
||||||
|
service_name: Some(originator.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
assert_eq!(thread.originator.as_deref(), Some(originator));
|
||||||
|
let started: codex_app_server_protocol::ThreadStartedNotification = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_notification("thread/started"),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
assert_eq!(started.thread, thread);
|
||||||
|
let thread_id = thread.id;
|
||||||
|
|
||||||
|
// The first turn exercises live metadata persistence, not rollout-file backfill.
|
||||||
|
let _: TurnStartResponse = mcp
|
||||||
|
.request(|request_id| ClientRequest::TurnStart {
|
||||||
|
request_id,
|
||||||
|
params: TurnStartParams {
|
||||||
|
thread_id: thread_id.clone(),
|
||||||
|
input: vec![UserInput::Text {
|
||||||
|
text: "Persist this thread".to_string(),
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let completed: codex_app_server_protocol::TurnCompletedNotification = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_notification("turn/completed"),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
assert_eq!(
|
||||||
|
completed.turn.status,
|
||||||
|
codex_app_server_protocol::TurnStatus::Completed
|
||||||
|
);
|
||||||
|
|
||||||
|
for restart in [false, true] {
|
||||||
|
if restart {
|
||||||
|
assert!(
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, mcp.shutdown_gracefully())
|
||||||
|
.await??
|
||||||
|
.success()
|
||||||
|
);
|
||||||
|
mcp = init_mcp(codex_home.path()).await?;
|
||||||
|
}
|
||||||
|
let response: ThreadListResponse = mcp
|
||||||
|
.request(|request_id| ClientRequest::ThreadList {
|
||||||
|
request_id,
|
||||||
|
params: serde_json::from_value(json!({"useStateDbOnly": true}))
|
||||||
|
.expect("valid list params"),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
response
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.map(|thread| (thread.id, thread.originator))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![(thread_id.clone(), Some(originator.to_string()))],
|
||||||
|
);
|
||||||
|
let read: ThreadReadResponse = mcp
|
||||||
|
.request(|request_id| ClientRequest::ThreadRead {
|
||||||
|
request_id,
|
||||||
|
params: ThreadReadParams {
|
||||||
|
thread_id: thread_id.clone(),
|
||||||
|
include_turns: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
assert_eq!(read.thread.originator.as_deref(), Some(originator));
|
||||||
|
}
|
||||||
|
let resumed: ThreadResumeResponse = mcp
|
||||||
|
.request(|request_id| ClientRequest::ThreadResume {
|
||||||
|
request_id,
|
||||||
|
params: ThreadResumeParams {
|
||||||
|
thread_id: thread_id.clone(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
assert_eq!(resumed.thread.originator.as_deref(), Some(originator));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn thread_list_invalid_cursor_returns_error() -> Result<()> {
|
async fn thread_list_invalid_cursor_returns_error() -> Result<()> {
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
@@ -2607,6 +2751,7 @@ async fn thread_list_invalid_cursor_returns_error() -> Result<()> {
|
|||||||
|
|
||||||
let request_id = mcp
|
let request_id = mcp
|
||||||
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
.send_thread_list_request(codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: Some("not-a-cursor".to_string()),
|
cursor: Some("not-a-cursor".to_string()),
|
||||||
limit: Some(2),
|
limit: Some(2),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ async fn thread_section_move_pins_before_first_turn() -> Result<()> {
|
|||||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(move_id)).await??;
|
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(move_id)).await??;
|
||||||
|
|
||||||
let list_params = ThreadListParams {
|
let list_params = ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(100),
|
limit: Some(100),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -261,6 +262,7 @@ async fn thread_section_move_pins_and_unpins_with_filtered_recency_pagination()
|
|||||||
}
|
}
|
||||||
|
|
||||||
let list_params = ThreadListParams {
|
let list_params = ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(1),
|
limit: Some(1),
|
||||||
sort_key: Some(ThreadSortKey::RecencyAt),
|
sort_key: Some(ThreadSortKey::RecencyAt),
|
||||||
@@ -452,6 +454,7 @@ async fn thread_sections_preserve_server_owned_manual_order_across_moves_and_res
|
|||||||
}
|
}
|
||||||
|
|
||||||
let list_params = ThreadListParams {
|
let list_params = ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: Some(ThreadSortKey::SectionPosition),
|
sort_key: Some(ThreadSortKey::SectionPosition),
|
||||||
|
|||||||
@@ -293,6 +293,7 @@ async fn paginated_stored_thread_routes_projected_turns() -> Result<()> {
|
|||||||
|
|
||||||
let list_id = mcp
|
let list_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(50),
|
limit: Some(50),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -984,6 +985,7 @@ async fn thread_list_includes_store_thread_without_rollout_path() -> Result<()>
|
|||||||
.request(ClientRequest::ThreadList {
|
.request(ClientRequest::ThreadList {
|
||||||
request_id: RequestId::Integer(1),
|
request_id: RequestId::Integer(1),
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
@@ -1364,6 +1366,7 @@ async fn paginated_thread_name_set_is_reflected_in_read_list_and_metadata_resume
|
|||||||
// List should also surface the name.
|
// List should also surface the name.
|
||||||
let list_id = mcp
|
let list_id = mcp
|
||||||
.send_thread_list_request(ThreadListParams {
|
.send_thread_list_request(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(50),
|
limit: Some(50),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
|
|||||||
@@ -343,6 +343,7 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
|
|||||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||||
model: Some("gpt-5.2".to_string()),
|
model: Some("gpt-5.2".to_string()),
|
||||||
thread_source: Some(ThreadSource::User),
|
thread_source: Some(ThreadSource::User),
|
||||||
|
service_name: Some("codex_work_desktop".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -383,6 +384,7 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
|
|||||||
);
|
);
|
||||||
assert_eq!(thread.status, ThreadStatus::Idle);
|
assert_eq!(thread.status, ThreadStatus::Idle);
|
||||||
assert_eq!(thread.thread_source, Some(ThreadSource::User));
|
assert_eq!(thread.thread_source, Some(ThreadSource::User));
|
||||||
|
assert_eq!(thread.originator.as_deref(), Some("codex_work_desktop"));
|
||||||
let thread_path = thread.path.clone().expect("thread path should be present");
|
let thread_path = thread.path.clone().expect("thread path should be present");
|
||||||
assert!(thread_path.is_absolute(), "thread path should be absolute");
|
assert!(thread_path.is_absolute(), "thread path should be absolute");
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -4382,6 +4382,7 @@ async fn direct_input_to_multi_agent_v2_subagent_is_rejected(
|
|||||||
.request(|request_id| ClientRequest::ThreadList {
|
.request(|request_id| ClientRequest::ThreadList {
|
||||||
request_id,
|
request_id,
|
||||||
params: codex_app_server_protocol::ThreadListParams {
|
params: codex_app_server_protocol::ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(10),
|
limit: Some(10),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ use tempfile::TempDir;
|
|||||||
|
|
||||||
fn stored_thread(cwd: &str, title: &str, first_user_message: &str) -> StoredThread {
|
fn stored_thread(cwd: &str, title: &str, first_user_message: &str) -> StoredThread {
|
||||||
StoredThread {
|
StoredThread {
|
||||||
|
originator: None,
|
||||||
thread_id: ThreadId::new(),
|
thread_id: ThreadId::new(),
|
||||||
extra_config: None,
|
extra_config: None,
|
||||||
rollout_path: Some(PathBuf::from("/tmp/rollout.jsonl")),
|
rollout_path: Some(PathBuf::from("/tmp/rollout.jsonl")),
|
||||||
|
|||||||
@@ -1604,6 +1604,7 @@ async fn resolve_resume_thread_id(
|
|||||||
ClientRequest::ThreadList {
|
ClientRequest::ThreadList {
|
||||||
request_id: RequestId::Integer(0),
|
request_id: RequestId::Integer(0),
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor,
|
cursor,
|
||||||
limit: Some(100),
|
limit: Some(100),
|
||||||
sort_key: Some(ThreadSortKey::UpdatedAt),
|
sort_key: Some(ThreadSortKey::UpdatedAt),
|
||||||
@@ -1689,6 +1690,7 @@ async fn resolve_resume_thread_id(
|
|||||||
ClientRequest::ThreadList {
|
ClientRequest::ThreadList {
|
||||||
request_id: RequestId::Integer(0),
|
request_id: RequestId::Integer(0),
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor,
|
cursor,
|
||||||
limit: Some(100),
|
limit: Some(100),
|
||||||
sort_key: Some(ThreadSortKey::UpdatedAt),
|
sort_key: Some(ThreadSortKey::UpdatedAt),
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
|||||||
path: None,
|
path: None,
|
||||||
cwd: test_path_buf("/tmp/project").abs(),
|
cwd: test_path_buf("/tmp/project").abs(),
|
||||||
cli_version: "0.0.0-test".to_string(),
|
cli_version: "0.0.0-test".to_string(),
|
||||||
|
originator: None,
|
||||||
source: codex_app_server_protocol::SessionSource::Exec,
|
source: codex_app_server_protocol::SessionSource::Exec,
|
||||||
can_accept_direct_input: None,
|
can_accept_direct_input: None,
|
||||||
thread_source: None,
|
thread_source: None,
|
||||||
@@ -842,6 +843,7 @@ async fn session_configured_from_thread_response_preserves_parent_thread_id() {
|
|||||||
fn sample_thread_start_response() -> ThreadStartResponse {
|
fn sample_thread_start_response() -> ThreadStartResponse {
|
||||||
ThreadStartResponse {
|
ThreadStartResponse {
|
||||||
thread: codex_app_server_protocol::Thread {
|
thread: codex_app_server_protocol::Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ pub struct ThreadsPage {
|
|||||||
/// Summary information for a thread rollout file.
|
/// Summary information for a thread rollout file.
|
||||||
#[derive(Debug, PartialEq, Default)]
|
#[derive(Debug, PartialEq, Default)]
|
||||||
pub struct ThreadItem {
|
pub struct ThreadItem {
|
||||||
|
/// Originator recorded at creation, if available.
|
||||||
|
pub originator: Option<String>,
|
||||||
/// Absolute path to the rollout file.
|
/// Absolute path to the rollout file.
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
/// Thread ID from session metadata.
|
/// Thread ID from session metadata.
|
||||||
@@ -106,6 +108,7 @@ pub type ConversationsPage = ThreadsPage;
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct HeadTailSummary {
|
struct HeadTailSummary {
|
||||||
|
originator: Option<String>,
|
||||||
saw_session_meta: bool,
|
saw_session_meta: bool,
|
||||||
thread_id: Option<ThreadId>,
|
thread_id: Option<ThreadId>,
|
||||||
first_user_message: Option<String>,
|
first_user_message: Option<String>,
|
||||||
@@ -815,6 +818,7 @@ async fn build_thread_item(
|
|||||||
// Apply filters: must have session meta and a discoverable preview.
|
// Apply filters: must have session meta and a discoverable preview.
|
||||||
if summary.saw_session_meta && summary.preview.is_some() {
|
if summary.saw_session_meta && summary.preview.is_some() {
|
||||||
let HeadTailSummary {
|
let HeadTailSummary {
|
||||||
|
originator,
|
||||||
thread_id,
|
thread_id,
|
||||||
first_user_message,
|
first_user_message,
|
||||||
preview,
|
preview,
|
||||||
@@ -837,6 +841,7 @@ async fn build_thread_item(
|
|||||||
summary_updated_at = updated_at.or_else(|| created_at.clone());
|
summary_updated_at = updated_at.or_else(|| created_at.clone());
|
||||||
}
|
}
|
||||||
return Some(ThreadItem {
|
return Some(ThreadItem {
|
||||||
|
originator,
|
||||||
path,
|
path,
|
||||||
thread_id,
|
thread_id,
|
||||||
first_user_message,
|
first_user_message,
|
||||||
@@ -1146,6 +1151,8 @@ async fn read_head_summary(path: &Path, head_limit: usize) -> io::Result<HeadTai
|
|||||||
match rollout_line.item {
|
match rollout_line.item {
|
||||||
RolloutItem::SessionMeta(session_meta_line) => {
|
RolloutItem::SessionMeta(session_meta_line) => {
|
||||||
if !summary.saw_session_meta {
|
if !summary.saw_session_meta {
|
||||||
|
summary.originator = (!session_meta_line.meta.originator.is_empty())
|
||||||
|
.then(|| session_meta_line.meta.originator.clone());
|
||||||
summary.source = Some(session_meta_line.meta.source.clone());
|
summary.source = Some(session_meta_line.meta.source.clone());
|
||||||
summary.history_mode = session_meta_line.meta.history_mode;
|
summary.history_mode = session_meta_line.meta.history_mode;
|
||||||
summary.parent_thread_id = session_meta_line.meta.parent_thread_id;
|
summary.parent_thread_id = session_meta_line.meta.parent_thread_id;
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ pub(crate) fn builder_from_session_meta(
|
|||||||
session_meta.meta.source.clone(),
|
session_meta.meta.source.clone(),
|
||||||
);
|
);
|
||||||
builder.history_mode = session_meta.meta.history_mode;
|
builder.history_mode = session_meta.meta.history_mode;
|
||||||
|
builder.originator =
|
||||||
|
(!session_meta.meta.originator.is_empty()).then(|| session_meta.meta.originator.clone());
|
||||||
builder.model_provider = session_meta.meta.model_provider.clone();
|
builder.model_provider = session_meta.meta.model_provider.clone();
|
||||||
builder.agent_nickname = session_meta.meta.agent_nickname.clone();
|
builder.agent_nickname = session_meta.meta.agent_nickname.clone();
|
||||||
builder.agent_role = session_meta.meta.agent_role.clone();
|
builder.agent_role = session_meta.meta.agent_role.clone();
|
||||||
|
|||||||
@@ -1282,6 +1282,7 @@ async fn fill_missing_thread_item_metadata_from_state_db(
|
|||||||
|
|
||||||
fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadItem) {
|
fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadItem) {
|
||||||
let ThreadItem {
|
let ThreadItem {
|
||||||
|
originator,
|
||||||
path: _state_path,
|
path: _state_path,
|
||||||
thread_id: _state_thread_id,
|
thread_id: _state_thread_id,
|
||||||
first_user_message,
|
first_user_message,
|
||||||
@@ -1306,6 +1307,10 @@ fn fill_missing_thread_item_metadata(item: &mut ThreadItem, state_item: ThreadIt
|
|||||||
recency_at,
|
recency_at,
|
||||||
} = state_item;
|
} = state_item;
|
||||||
|
|
||||||
|
if item.originator.is_none() {
|
||||||
|
item.originator = originator;
|
||||||
|
}
|
||||||
|
|
||||||
if item.first_user_message.is_none() {
|
if item.first_user_message.is_none() {
|
||||||
item.first_user_message = first_user_message;
|
item.first_user_message = first_user_message;
|
||||||
}
|
}
|
||||||
@@ -2024,6 +2029,7 @@ fn thread_item_from_state_metadata(
|
|||||||
parent_thread_id: Option<ThreadId>,
|
parent_thread_id: Option<ThreadId>,
|
||||||
) -> ThreadItem {
|
) -> ThreadItem {
|
||||||
ThreadItem {
|
ThreadItem {
|
||||||
|
originator: item.originator,
|
||||||
path: item.rollout_path,
|
path: item.rollout_path,
|
||||||
thread_id: Some(item.id),
|
thread_id: Some(item.id),
|
||||||
first_user_message: item.first_user_message,
|
first_user_message: item.first_user_message,
|
||||||
|
|||||||
@@ -1594,6 +1594,7 @@ fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fi
|
|||||||
let filesystem_path = PathBuf::from("/tmp/filesystem-rollout.jsonl");
|
let filesystem_path = PathBuf::from("/tmp/filesystem-rollout.jsonl");
|
||||||
let state_path = PathBuf::from("/tmp/state-rollout.jsonl");
|
let state_path = PathBuf::from("/tmp/state-rollout.jsonl");
|
||||||
let mut item = ThreadItem {
|
let mut item = ThreadItem {
|
||||||
|
originator: None,
|
||||||
path: filesystem_path.clone(),
|
path: filesystem_path.clone(),
|
||||||
thread_id: Some(filesystem_thread_id),
|
thread_id: Some(filesystem_thread_id),
|
||||||
first_user_message: Some("filesystem message".to_string()),
|
first_user_message: Some("filesystem message".to_string()),
|
||||||
@@ -1621,6 +1622,7 @@ fn fill_missing_thread_item_metadata_preserves_identity_and_prefers_state_git_fi
|
|||||||
updated_at: None,
|
updated_at: None,
|
||||||
};
|
};
|
||||||
let state_item = ThreadItem {
|
let state_item = ThreadItem {
|
||||||
|
originator: None,
|
||||||
path: state_path,
|
path: state_path,
|
||||||
thread_id: Some(state_thread_id),
|
thread_id: Some(state_thread_id),
|
||||||
first_user_message: Some("state message".to_string()),
|
first_user_message: Some("state message".to_string()),
|
||||||
|
|||||||
@@ -719,6 +719,7 @@ async fn test_list_conversations_latest_first() {
|
|||||||
let expected = ThreadsPage {
|
let expected = ThreadsPage {
|
||||||
items: vec![
|
items: vec![
|
||||||
ThreadItem {
|
ThreadItem {
|
||||||
|
originator: Some("test_originator".to_string()),
|
||||||
path: p1,
|
path: p1,
|
||||||
thread_id: Some(thread_id_from_uuid(u3)),
|
thread_id: Some(thread_id_from_uuid(u3)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
@@ -743,6 +744,7 @@ async fn test_list_conversations_latest_first() {
|
|||||||
updated_at: updated_times.first().cloned().flatten(),
|
updated_at: updated_times.first().cloned().flatten(),
|
||||||
},
|
},
|
||||||
ThreadItem {
|
ThreadItem {
|
||||||
|
originator: Some("test_originator".to_string()),
|
||||||
path: p2,
|
path: p2,
|
||||||
thread_id: Some(thread_id_from_uuid(u2)),
|
thread_id: Some(thread_id_from_uuid(u2)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
@@ -767,6 +769,7 @@ async fn test_list_conversations_latest_first() {
|
|||||||
updated_at: updated_times.get(1).cloned().flatten(),
|
updated_at: updated_times.get(1).cloned().flatten(),
|
||||||
},
|
},
|
||||||
ThreadItem {
|
ThreadItem {
|
||||||
|
originator: Some("test_originator".to_string()),
|
||||||
path: p3,
|
path: p3,
|
||||||
thread_id: Some(thread_id_from_uuid(u1)),
|
thread_id: Some(thread_id_from_uuid(u1)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
@@ -884,6 +887,7 @@ async fn test_pagination_cursor() {
|
|||||||
let expected_page1 = ThreadsPage {
|
let expected_page1 = ThreadsPage {
|
||||||
items: vec![
|
items: vec![
|
||||||
ThreadItem {
|
ThreadItem {
|
||||||
|
originator: Some("test_originator".to_string()),
|
||||||
path: p5,
|
path: p5,
|
||||||
thread_id: Some(thread_id_from_uuid(u5)),
|
thread_id: Some(thread_id_from_uuid(u5)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
@@ -908,6 +912,7 @@ async fn test_pagination_cursor() {
|
|||||||
updated_at: updated_page1.first().cloned().flatten(),
|
updated_at: updated_page1.first().cloned().flatten(),
|
||||||
},
|
},
|
||||||
ThreadItem {
|
ThreadItem {
|
||||||
|
originator: Some("test_originator".to_string()),
|
||||||
path: p4,
|
path: p4,
|
||||||
thread_id: Some(thread_id_from_uuid(u4)),
|
thread_id: Some(thread_id_from_uuid(u4)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
@@ -968,6 +973,7 @@ async fn test_pagination_cursor() {
|
|||||||
let expected_page2 = ThreadsPage {
|
let expected_page2 = ThreadsPage {
|
||||||
items: vec![
|
items: vec![
|
||||||
ThreadItem {
|
ThreadItem {
|
||||||
|
originator: Some("test_originator".to_string()),
|
||||||
path: p3,
|
path: p3,
|
||||||
thread_id: Some(thread_id_from_uuid(u3)),
|
thread_id: Some(thread_id_from_uuid(u3)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
@@ -992,6 +998,7 @@ async fn test_pagination_cursor() {
|
|||||||
updated_at: updated_page2.first().cloned().flatten(),
|
updated_at: updated_page2.first().cloned().flatten(),
|
||||||
},
|
},
|
||||||
ThreadItem {
|
ThreadItem {
|
||||||
|
originator: Some("test_originator".to_string()),
|
||||||
path: p2,
|
path: p2,
|
||||||
thread_id: Some(thread_id_from_uuid(u2)),
|
thread_id: Some(thread_id_from_uuid(u2)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
@@ -1044,6 +1051,7 @@ async fn test_pagination_cursor() {
|
|||||||
page3.items.iter().map(|i| i.updated_at.clone()).collect();
|
page3.items.iter().map(|i| i.updated_at.clone()).collect();
|
||||||
let expected_page3 = ThreadsPage {
|
let expected_page3 = ThreadsPage {
|
||||||
items: vec![ThreadItem {
|
items: vec![ThreadItem {
|
||||||
|
originator: Some("test_originator".to_string()),
|
||||||
path: p1,
|
path: p1,
|
||||||
thread_id: Some(thread_id_from_uuid(u1)),
|
thread_id: Some(thread_id_from_uuid(u1)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
@@ -1221,6 +1229,7 @@ async fn test_get_thread_contents() {
|
|||||||
.join(format!("rollout-2025-04-01T10-30-00-{uuid}.jsonl"));
|
.join(format!("rollout-2025-04-01T10-30-00-{uuid}.jsonl"));
|
||||||
let expected_page = ThreadsPage {
|
let expected_page = ThreadsPage {
|
||||||
items: vec![ThreadItem {
|
items: vec![ThreadItem {
|
||||||
|
originator: Some("test_originator".to_string()),
|
||||||
path: expected_path,
|
path: expected_path,
|
||||||
thread_id: Some(thread_id_from_uuid(uuid)),
|
thread_id: Some(thread_id_from_uuid(uuid)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
@@ -1626,6 +1635,7 @@ async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties() {
|
|||||||
let expected_page1 = ThreadsPage {
|
let expected_page1 = ThreadsPage {
|
||||||
items: vec![
|
items: vec![
|
||||||
ThreadItem {
|
ThreadItem {
|
||||||
|
originator: Some("test_originator".to_string()),
|
||||||
path: p3,
|
path: p3,
|
||||||
thread_id: Some(thread_id_from_uuid(u3)),
|
thread_id: Some(thread_id_from_uuid(u3)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
@@ -1650,6 +1660,7 @@ async fn test_timestamp_only_cursor_skips_same_second_filesystem_ties() {
|
|||||||
updated_at: updated_page1.first().cloned().flatten(),
|
updated_at: updated_page1.first().cloned().flatten(),
|
||||||
},
|
},
|
||||||
ThreadItem {
|
ThreadItem {
|
||||||
|
originator: Some("test_originator".to_string()),
|
||||||
path: p2,
|
path: p2,
|
||||||
thread_id: Some(thread_id_from_uuid(u2)),
|
thread_id: Some(thread_id_from_uuid(u2)),
|
||||||
first_user_message: Some("Hello from user".to_string()),
|
first_user_message: Some("Hello from user".to_string()),
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE threads ADD COLUMN originator TEXT;
|
||||||
@@ -71,6 +71,9 @@ fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &Sessi
|
|||||||
}
|
}
|
||||||
metadata.id = meta_line.meta.id;
|
metadata.id = meta_line.meta.id;
|
||||||
metadata.source = enum_to_string(&meta_line.meta.source);
|
metadata.source = enum_to_string(&meta_line.meta.source);
|
||||||
|
if metadata.originator.is_none() && !meta_line.meta.originator.is_empty() {
|
||||||
|
metadata.originator = Some(meta_line.meta.originator.clone());
|
||||||
|
}
|
||||||
// Later SessionMeta lines do not redefine the canonical history_mode.
|
// Later SessionMeta lines do not redefine the canonical history_mode.
|
||||||
metadata.thread_source = meta_line.meta.thread_source.clone();
|
metadata.thread_source = meta_line.meta.thread_source.clone();
|
||||||
metadata.agent_nickname = meta_line.meta.agent_nickname.clone();
|
metadata.agent_nickname = meta_line.meta.agent_nickname.clone();
|
||||||
@@ -696,6 +699,7 @@ mod tests {
|
|||||||
let id = ThreadId::from_string(&Uuid::from_u128(42).to_string()).expect("thread id");
|
let id = ThreadId::from_string(&Uuid::from_u128(42).to_string()).expect("thread id");
|
||||||
let created_at = DateTime::<Utc>::from_timestamp(1_735_689_600, 0).expect("timestamp");
|
let created_at = DateTime::<Utc>::from_timestamp(1_735_689_600, 0).expect("timestamp");
|
||||||
ThreadMetadata {
|
ThreadMetadata {
|
||||||
|
originator: None,
|
||||||
id,
|
id,
|
||||||
rollout_path: PathBuf::from("/tmp/a.jsonl"),
|
rollout_path: PathBuf::from("/tmp/a.jsonl"),
|
||||||
created_at,
|
created_at,
|
||||||
|
|||||||
@@ -123,6 +123,8 @@ pub struct ExtractionOutcome {
|
|||||||
/// Canonical persisted thread metadata.
|
/// Canonical persisted thread metadata.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ThreadMetadata {
|
pub struct ThreadMetadata {
|
||||||
|
/// Originator recorded at creation, if available.
|
||||||
|
pub originator: Option<String>,
|
||||||
/// The thread identifier.
|
/// The thread identifier.
|
||||||
pub id: ThreadId,
|
pub id: ThreadId,
|
||||||
/// The absolute rollout path on disk.
|
/// The absolute rollout path on disk.
|
||||||
@@ -190,6 +192,8 @@ pub struct ThreadMetadata {
|
|||||||
/// Builder data required to construct [`ThreadMetadata`] without parsing filenames.
|
/// Builder data required to construct [`ThreadMetadata`] without parsing filenames.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ThreadMetadataBuilder {
|
pub struct ThreadMetadataBuilder {
|
||||||
|
/// Originator recorded at creation, if available.
|
||||||
|
pub originator: Option<String>,
|
||||||
/// The thread identifier.
|
/// The thread identifier.
|
||||||
pub id: ThreadId,
|
pub id: ThreadId,
|
||||||
/// The absolute rollout path on disk.
|
/// The absolute rollout path on disk.
|
||||||
@@ -246,6 +250,7 @@ impl ThreadMetadataBuilder {
|
|||||||
created_at,
|
created_at,
|
||||||
updated_at: None,
|
updated_at: None,
|
||||||
recency_at: None,
|
recency_at: None,
|
||||||
|
originator: None,
|
||||||
source,
|
source,
|
||||||
history_mode: ThreadHistoryMode::Legacy,
|
history_mode: ThreadHistoryMode::Legacy,
|
||||||
thread_source: None,
|
thread_source: None,
|
||||||
@@ -279,6 +284,7 @@ impl ThreadMetadataBuilder {
|
|||||||
.map(canonicalize_datetime)
|
.map(canonicalize_datetime)
|
||||||
.unwrap_or(updated_at);
|
.unwrap_or(updated_at);
|
||||||
ThreadMetadata {
|
ThreadMetadata {
|
||||||
|
originator: self.originator.clone(),
|
||||||
id: self.id,
|
id: self.id,
|
||||||
rollout_path: self.rollout_path.clone(),
|
rollout_path: self.rollout_path.clone(),
|
||||||
created_at,
|
created_at,
|
||||||
@@ -379,6 +385,9 @@ impl ThreadMetadata {
|
|||||||
if self.source != other.source {
|
if self.source != other.source {
|
||||||
diffs.push("source");
|
diffs.push("source");
|
||||||
}
|
}
|
||||||
|
if self.originator != other.originator {
|
||||||
|
diffs.push("originator");
|
||||||
|
}
|
||||||
if self.agent_nickname != other.agent_nickname {
|
if self.agent_nickname != other.agent_nickname {
|
||||||
diffs.push("agent_nickname");
|
diffs.push("agent_nickname");
|
||||||
}
|
}
|
||||||
@@ -458,6 +467,7 @@ fn canonicalize_datetime(dt: DateTime<Utc>) -> DateTime<Utc> {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct ThreadRow {
|
pub(crate) struct ThreadRow {
|
||||||
|
originator: Option<String>,
|
||||||
id: String,
|
id: String,
|
||||||
rollout_path: String,
|
rollout_path: String,
|
||||||
created_at: i64,
|
created_at: i64,
|
||||||
@@ -496,6 +506,7 @@ pub(crate) struct ThreadRow {
|
|||||||
impl ThreadRow {
|
impl ThreadRow {
|
||||||
pub(crate) fn try_from_row(row: &SqliteRow) -> Result<Self> {
|
pub(crate) fn try_from_row(row: &SqliteRow) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
originator: row.try_get("originator")?,
|
||||||
id: row.try_get("id")?,
|
id: row.try_get("id")?,
|
||||||
rollout_path: row.try_get("rollout_path")?,
|
rollout_path: row.try_get("rollout_path")?,
|
||||||
created_at: row.try_get("created_at")?,
|
created_at: row.try_get("created_at")?,
|
||||||
@@ -538,6 +549,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
|||||||
|
|
||||||
fn try_from(row: ThreadRow) -> std::result::Result<Self, Self::Error> {
|
fn try_from(row: ThreadRow) -> std::result::Result<Self, Self::Error> {
|
||||||
let ThreadRow {
|
let ThreadRow {
|
||||||
|
originator,
|
||||||
id,
|
id,
|
||||||
rollout_path,
|
rollout_path,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -595,6 +607,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
|
|||||||
};
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: ThreadId::try_from(id)?,
|
id: ThreadId::try_from(id)?,
|
||||||
|
originator,
|
||||||
rollout_path: PathBuf::from(rollout_path),
|
rollout_path: PathBuf::from(rollout_path),
|
||||||
created_at: epoch_millis_to_datetime(created_at)?,
|
created_at: epoch_millis_to_datetime(created_at)?,
|
||||||
updated_at: epoch_millis_to_datetime(updated_at)?,
|
updated_at: epoch_millis_to_datetime(updated_at)?,
|
||||||
@@ -704,6 +717,7 @@ mod tests {
|
|||||||
|
|
||||||
fn thread_row(reasoning_effort: Option<&str>) -> ThreadRow {
|
fn thread_row(reasoning_effort: Option<&str>) -> ThreadRow {
|
||||||
ThreadRow {
|
ThreadRow {
|
||||||
|
originator: None,
|
||||||
id: "00000000-0000-0000-0000-000000000123".to_string(),
|
id: "00000000-0000-0000-0000-000000000123".to_string(),
|
||||||
rollout_path: "/tmp/rollout-123.jsonl".to_string(),
|
rollout_path: "/tmp/rollout-123.jsonl".to_string(),
|
||||||
created_at: 1_700_000_000,
|
created_at: 1_700_000_000,
|
||||||
@@ -742,6 +756,7 @@ mod tests {
|
|||||||
|
|
||||||
fn expected_thread_metadata(reasoning_effort: Option<ReasoningEffort>) -> ThreadMetadata {
|
fn expected_thread_metadata(reasoning_effort: Option<ReasoningEffort>) -> ThreadMetadata {
|
||||||
ThreadMetadata {
|
ThreadMetadata {
|
||||||
|
originator: None,
|
||||||
id: ThreadId::from_string("00000000-0000-0000-0000-000000000123")
|
id: ThreadId::from_string("00000000-0000-0000-0000-000000000123")
|
||||||
.expect("valid thread id"),
|
.expect("valid thread id"),
|
||||||
rollout_path: PathBuf::from("/tmp/rollout-123.jsonl"),
|
rollout_path: PathBuf::from("/tmp/rollout-123.jsonl"),
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ SELECT
|
|||||||
threads.updated_at_ms AS updated_at,
|
threads.updated_at_ms AS updated_at,
|
||||||
threads.recency_at_ms AS recency_at,
|
threads.recency_at_ms AS recency_at,
|
||||||
threads.source,
|
threads.source,
|
||||||
|
threads.originator,
|
||||||
threads.history_mode,
|
threads.history_mode,
|
||||||
threads.thread_source,
|
threads.thread_source,
|
||||||
threads.agent_path,
|
threads.agent_path,
|
||||||
@@ -567,6 +568,7 @@ SELECT
|
|||||||
threads.updated_at_ms AS updated_at,
|
threads.updated_at_ms AS updated_at,
|
||||||
threads.recency_at_ms AS recency_at,
|
threads.recency_at_ms AS recency_at,
|
||||||
threads.source,
|
threads.source,
|
||||||
|
threads.originator,
|
||||||
threads.history_mode,
|
threads.history_mode,
|
||||||
threads.thread_source,
|
threads.thread_source,
|
||||||
threads.agent_nickname,
|
threads.agent_nickname,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ pub(super) fn test_thread_metadata(
|
|||||||
) -> ThreadMetadata {
|
) -> ThreadMetadata {
|
||||||
let now = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).expect("timestamp");
|
let now = DateTime::<Utc>::from_timestamp(1_700_000_000, 0).expect("timestamp");
|
||||||
ThreadMetadata {
|
ThreadMetadata {
|
||||||
|
originator: None,
|
||||||
id: thread_id,
|
id: thread_id,
|
||||||
rollout_path: codex_home.join(format!("rollout-{thread_id}.jsonl")),
|
rollout_path: codex_home.join(format!("rollout-{thread_id}.jsonl")),
|
||||||
created_at: now,
|
created_at: now,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ SELECT
|
|||||||
threads.updated_at_ms AS updated_at,
|
threads.updated_at_ms AS updated_at,
|
||||||
threads.recency_at_ms AS recency_at,
|
threads.recency_at_ms AS recency_at,
|
||||||
threads.source,
|
threads.source,
|
||||||
|
threads.originator,
|
||||||
threads.history_mode,
|
threads.history_mode,
|
||||||
threads.thread_source,
|
threads.thread_source,
|
||||||
threads.agent_nickname,
|
threads.agent_nickname,
|
||||||
@@ -618,6 +619,7 @@ INSERT INTO threads (
|
|||||||
updated_at_ms,
|
updated_at_ms,
|
||||||
recency_at_ms,
|
recency_at_ms,
|
||||||
source,
|
source,
|
||||||
|
originator,
|
||||||
history_mode,
|
history_mode,
|
||||||
thread_source,
|
thread_source,
|
||||||
agent_nickname,
|
agent_nickname,
|
||||||
@@ -645,7 +647,7 @@ INSERT INTO threads (
|
|||||||
git_origin_url,
|
git_origin_url,
|
||||||
memory_mode,
|
memory_mode,
|
||||||
project_id
|
project_id
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(id) DO NOTHING
|
ON CONFLICT(id) DO NOTHING
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
@@ -658,6 +660,7 @@ ON CONFLICT(id) DO NOTHING
|
|||||||
.bind(datetime_to_epoch_millis(updated_at))
|
.bind(datetime_to_epoch_millis(updated_at))
|
||||||
.bind(datetime_to_epoch_millis(recency_at))
|
.bind(datetime_to_epoch_millis(recency_at))
|
||||||
.bind(metadata.source.as_str())
|
.bind(metadata.source.as_str())
|
||||||
|
.bind(metadata.originator.as_deref())
|
||||||
.bind(metadata.history_mode.as_str())
|
.bind(metadata.history_mode.as_str())
|
||||||
.bind(
|
.bind(
|
||||||
metadata
|
metadata
|
||||||
@@ -895,6 +898,7 @@ INSERT INTO threads (
|
|||||||
updated_at_ms,
|
updated_at_ms,
|
||||||
recency_at_ms,
|
recency_at_ms,
|
||||||
source,
|
source,
|
||||||
|
originator,
|
||||||
history_mode,
|
history_mode,
|
||||||
thread_source,
|
thread_source,
|
||||||
agent_nickname,
|
agent_nickname,
|
||||||
@@ -922,7 +926,7 @@ INSERT INTO threads (
|
|||||||
git_origin_url,
|
git_origin_url,
|
||||||
memory_mode,
|
memory_mode,
|
||||||
project_id
|
project_id
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
rollout_path = excluded.rollout_path,
|
rollout_path = excluded.rollout_path,
|
||||||
created_at = excluded.created_at,
|
created_at = excluded.created_at,
|
||||||
@@ -932,6 +936,7 @@ ON CONFLICT(id) DO UPDATE SET
|
|||||||
updated_at_ms = excluded.updated_at_ms,
|
updated_at_ms = excluded.updated_at_ms,
|
||||||
recency_at_ms = threads.recency_at_ms,
|
recency_at_ms = threads.recency_at_ms,
|
||||||
source = excluded.source,
|
source = excluded.source,
|
||||||
|
originator = COALESCE(threads.originator, excluded.originator),
|
||||||
-- Paginated history is a one-way promotion; stale legacy metadata must not downgrade it.
|
-- Paginated history is a one-way promotion; stale legacy metadata must not downgrade it.
|
||||||
history_mode = CASE
|
history_mode = CASE
|
||||||
WHEN threads.history_mode = 'paginated' THEN threads.history_mode
|
WHEN threads.history_mode = 'paginated' THEN threads.history_mode
|
||||||
@@ -968,6 +973,7 @@ ON CONFLICT(id) DO UPDATE SET
|
|||||||
.bind(datetime_to_epoch_millis(updated_at))
|
.bind(datetime_to_epoch_millis(updated_at))
|
||||||
.bind(datetime_to_epoch_millis(insert_recency_at))
|
.bind(datetime_to_epoch_millis(insert_recency_at))
|
||||||
.bind(metadata.source.as_str())
|
.bind(metadata.source.as_str())
|
||||||
|
.bind(metadata.originator.as_deref())
|
||||||
.bind(metadata.history_mode.as_str())
|
.bind(metadata.history_mode.as_str())
|
||||||
.bind(
|
.bind(
|
||||||
metadata
|
metadata
|
||||||
@@ -1273,6 +1279,7 @@ SELECT
|
|||||||
threads.updated_at_ms AS updated_at,
|
threads.updated_at_ms AS updated_at,
|
||||||
threads.recency_at_ms AS recency_at,
|
threads.recency_at_ms AS recency_at,
|
||||||
threads.source,
|
threads.source,
|
||||||
|
threads.originator,
|
||||||
threads.history_mode,
|
threads.history_mode,
|
||||||
threads.thread_source,
|
threads.thread_source,
|
||||||
threads.agent_nickname,
|
threads.agent_nickname,
|
||||||
@@ -2695,7 +2702,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn upsert_thread_preserves_existing_git_fields_atomically() {
|
async fn upsert_thread_preserves_existing_git_and_originator_atomically() {
|
||||||
let codex_home = unique_temp_dir();
|
let codex_home = unique_temp_dir();
|
||||||
let runtime = StateRuntime::init(
|
let runtime = StateRuntime::init(
|
||||||
crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()),
|
crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()),
|
||||||
@@ -2719,6 +2726,7 @@ mod tests {
|
|||||||
.expect("initial upsert should succeed");
|
.expect("initial upsert should succeed");
|
||||||
|
|
||||||
let mut rollout_metadata = metadata.clone();
|
let mut rollout_metadata = metadata.clone();
|
||||||
|
rollout_metadata.originator = Some("recorded_client".to_string());
|
||||||
rollout_metadata.git_sha = Some("rollout-sha".to_string());
|
rollout_metadata.git_sha = Some("rollout-sha".to_string());
|
||||||
rollout_metadata.git_branch = Some("rollout-branch".to_string());
|
rollout_metadata.git_branch = Some("rollout-branch".to_string());
|
||||||
rollout_metadata.git_origin_url = Some(
|
rollout_metadata.git_origin_url = Some(
|
||||||
@@ -2736,12 +2744,27 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("thread should load")
|
.expect("thread should load")
|
||||||
.expect("thread should exist");
|
.expect("thread should exist");
|
||||||
|
assert_eq!(persisted.originator.as_deref(), Some("recorded_client"));
|
||||||
assert_eq!(persisted.git_sha.as_deref(), Some("sqlite-sha"));
|
assert_eq!(persisted.git_sha.as_deref(), Some("sqlite-sha"));
|
||||||
assert_eq!(persisted.git_branch.as_deref(), Some("sqlite-branch"));
|
assert_eq!(persisted.git_branch.as_deref(), Some("sqlite-branch"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
persisted.git_origin_url.as_deref(),
|
persisted.git_origin_url.as_deref(),
|
||||||
Some("git@example.com:openai/codex.git")
|
Some("git@example.com:openai/codex.git")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for incoming_originator in [None, Some("resume_client")] {
|
||||||
|
rollout_metadata.originator = incoming_originator.map(str::to_owned);
|
||||||
|
runtime
|
||||||
|
.upsert_thread(&rollout_metadata)
|
||||||
|
.await
|
||||||
|
.expect("later upsert should succeed");
|
||||||
|
let persisted = runtime
|
||||||
|
.get_thread(thread_id)
|
||||||
|
.await
|
||||||
|
.expect("thread should load")
|
||||||
|
.expect("thread should exist");
|
||||||
|
assert_eq!(persisted.originator.as_deref(), Some("recorded_client"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1033,6 +1033,7 @@ fn stored_thread_from_state(
|
|||||||
});
|
});
|
||||||
|
|
||||||
Ok(StoredThread {
|
Ok(StoredThread {
|
||||||
|
originator: (!created.originator.is_empty()).then(|| created.originator.clone()),
|
||||||
thread_id,
|
thread_id,
|
||||||
extra_config: created.extra_config.clone(),
|
extra_config: created.extra_config.clone(),
|
||||||
rollout_path: metadata
|
rollout_path: metadata
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ pub(super) fn stored_thread_from_rollout_item(
|
|||||||
project_id: item.project_id,
|
project_id: item.project_id,
|
||||||
cwd: item.cwd.unwrap_or_default(),
|
cwd: item.cwd.unwrap_or_default(),
|
||||||
cli_version: item.cli_version.unwrap_or_default(),
|
cli_version: item.cli_version.unwrap_or_default(),
|
||||||
|
originator: item.originator,
|
||||||
source,
|
source,
|
||||||
history_mode: item.history_mode,
|
history_mode: item.history_mode,
|
||||||
thread_source: None,
|
thread_source: None,
|
||||||
|
|||||||
@@ -349,6 +349,11 @@ pub(super) async fn stored_thread_from_sqlite_metadata(
|
|||||||
.unwrap_or(metadata.history_mode);
|
.unwrap_or(metadata.history_mode);
|
||||||
let name = thread_name_from_metadata(store, &metadata, history_mode).await;
|
let name = thread_name_from_metadata(store, &metadata, history_mode).await;
|
||||||
let mut thread = stored_thread_from_state_metadata(store, metadata, parent_thread_id);
|
let mut thread = stored_thread_from_state_metadata(store, metadata, parent_thread_id);
|
||||||
|
thread.originator = thread.originator.or_else(|| {
|
||||||
|
session_meta
|
||||||
|
.map(|meta| meta.originator)
|
||||||
|
.filter(|originator| !originator.is_empty())
|
||||||
|
});
|
||||||
thread.forked_from_id = forked_from_id;
|
thread.forked_from_id = forked_from_id;
|
||||||
thread.history_mode = history_mode;
|
thread.history_mode = history_mode;
|
||||||
thread.name = name;
|
thread.name = name;
|
||||||
@@ -397,6 +402,7 @@ pub(super) fn stored_thread_from_state_metadata(
|
|||||||
project_id: metadata.project_id,
|
project_id: metadata.project_id,
|
||||||
cwd: metadata.cwd,
|
cwd: metadata.cwd,
|
||||||
cli_version: metadata.cli_version,
|
cli_version: metadata.cli_version,
|
||||||
|
originator: metadata.originator,
|
||||||
source: parse_session_source(&metadata.source),
|
source: parse_session_source(&metadata.source),
|
||||||
history_mode: metadata.history_mode,
|
history_mode: metadata.history_mode,
|
||||||
thread_source: metadata.thread_source,
|
thread_source: metadata.thread_source,
|
||||||
@@ -496,6 +502,7 @@ fn stored_thread_from_meta_line(
|
|||||||
project_id: None,
|
project_id: None,
|
||||||
cwd: meta_line.meta.cwd,
|
cwd: meta_line.meta.cwd,
|
||||||
cli_version: meta_line.meta.cli_version,
|
cli_version: meta_line.meta.cli_version,
|
||||||
|
originator: (!meta_line.meta.originator.is_empty()).then_some(meta_line.meta.originator),
|
||||||
source: meta_line.meta.source,
|
source: meta_line.meta.source,
|
||||||
history_mode: meta_line.meta.history_mode,
|
history_mode: meta_line.meta.history_mode,
|
||||||
thread_source: meta_line.meta.thread_source,
|
thread_source: meta_line.meta.thread_source,
|
||||||
@@ -916,6 +923,7 @@ mod tests {
|
|||||||
thread.first_user_message.as_deref(),
|
thread.first_user_message.as_deref(),
|
||||||
thread.model_provider.as_str(),
|
thread.model_provider.as_str(),
|
||||||
thread.model.as_deref(),
|
thread.model.as_deref(),
|
||||||
|
thread.originator.as_deref(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Some("Canonical SQLite name"),
|
Some("Canonical SQLite name"),
|
||||||
@@ -923,6 +931,7 @@ mod tests {
|
|||||||
Some("Canonical SQLite first user message"),
|
Some("Canonical SQLite first user message"),
|
||||||
"Canonical SQLite provider",
|
"Canonical SQLite provider",
|
||||||
Some("Canonical SQLite model"),
|
Some("Canonical SQLite model"),
|
||||||
|
Some("test_originator"),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1193,7 +1202,7 @@ mod tests {
|
|||||||
std::fs::create_dir_all(&day_dir).expect("sessions dir");
|
std::fs::create_dir_all(&day_dir).expect("sessions dir");
|
||||||
let rollout_path = day_dir.join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));
|
let rollout_path = day_dir.join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl"));
|
||||||
let mut file = std::fs::File::create(&rollout_path).expect("session file");
|
let mut file = std::fs::File::create(&rollout_path).expect("session file");
|
||||||
let meta = serde_json::json!({
|
let mut meta = serde_json::json!({
|
||||||
"timestamp": "2025-01-03T12-00-00",
|
"timestamp": "2025-01-03T12-00-00",
|
||||||
"type": "session_meta",
|
"type": "session_meta",
|
||||||
"payload": {
|
"payload": {
|
||||||
@@ -1248,9 +1257,28 @@ mod tests {
|
|||||||
assert_eq!(thread.model_provider, "sqlite-provider");
|
assert_eq!(thread.model_provider, "sqlite-provider");
|
||||||
assert_eq!(thread.cwd, home.path().join("workspace"));
|
assert_eq!(thread.cwd, home.path().join("workspace"));
|
||||||
assert_eq!(thread.cli_version, "sqlite-cli");
|
assert_eq!(thread.cli_version, "sqlite-cli");
|
||||||
|
assert_eq!(thread.originator.as_deref(), Some("test_originator"));
|
||||||
let history = thread.history.expect("history should load");
|
let history = thread.history.expect("history should load");
|
||||||
assert_eq!(history.thread_id, thread_id);
|
assert_eq!(history.thread_id, thread_id);
|
||||||
assert_eq!(history.items.len(), 1);
|
assert_eq!(history.items.len(), 1);
|
||||||
|
|
||||||
|
for (sqlite_originator, rollout_originator, expected_originator) in [
|
||||||
|
(
|
||||||
|
Some("saved_client"),
|
||||||
|
"test_originator",
|
||||||
|
Some("saved_client"),
|
||||||
|
),
|
||||||
|
(None, "", None),
|
||||||
|
] {
|
||||||
|
meta["payload"]["originator"] = serde_json::json!(rollout_originator);
|
||||||
|
std::fs::write(&metadata.rollout_path, format!("{meta}\n"))
|
||||||
|
.expect("write session meta");
|
||||||
|
metadata.originator = sqlite_originator.map(str::to_owned);
|
||||||
|
let thread = stored_thread_from_sqlite_metadata(&store, metadata.clone())
|
||||||
|
.await
|
||||||
|
.expect("read SQLite and session metadata");
|
||||||
|
assert_eq!(thread.originator.as_deref(), expected_originator);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ async fn apply_metadata_update(
|
|||||||
if let Some(source) = patch.source {
|
if let Some(source) = patch.source {
|
||||||
metadata.source = enum_to_string(&source);
|
metadata.source = enum_to_string(&source);
|
||||||
}
|
}
|
||||||
|
metadata.originator = metadata.originator.or(patch.originator);
|
||||||
if let Some(thread_source) = patch.thread_source {
|
if let Some(thread_source) = patch.thread_source {
|
||||||
metadata.thread_source = thread_source;
|
metadata.thread_source = thread_source;
|
||||||
}
|
}
|
||||||
@@ -672,6 +673,7 @@ fn has_observed_metadata_facts(patch: &ThreadMetadataPatch) -> bool {
|
|||||||
|| patch.reasoning_effort.is_some()
|
|| patch.reasoning_effort.is_some()
|
||||||
|| patch.created_at.is_some()
|
|| patch.created_at.is_some()
|
||||||
|| patch.source.is_some()
|
|| patch.source.is_some()
|
||||||
|
|| patch.originator.is_some()
|
||||||
|| patch.thread_source.is_some()
|
|| patch.thread_source.is_some()
|
||||||
|| patch.agent_nickname.is_some()
|
|| patch.agent_nickname.is_some()
|
||||||
|| patch.agent_role.is_some()
|
|| patch.agent_role.is_some()
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ impl ThreadMetadataSync {
|
|||||||
created_at: Some(created_at),
|
created_at: Some(created_at),
|
||||||
updated_at: Some(created_at),
|
updated_at: Some(created_at),
|
||||||
source: Some(params.source.clone()),
|
source: Some(params.source.clone()),
|
||||||
|
originator: (!params.originator.is_empty()).then(|| params.originator.clone()),
|
||||||
thread_source: Some(params.thread_source.clone()),
|
thread_source: Some(params.thread_source.clone()),
|
||||||
agent_nickname: Some(params.source.get_nickname()),
|
agent_nickname: Some(params.source.get_nickname()),
|
||||||
agent_role: Some(params.source.get_agent_role()),
|
agent_role: Some(params.source.get_agent_role()),
|
||||||
@@ -228,6 +229,9 @@ impl ThreadMetadataSync {
|
|||||||
RolloutItem::SessionMeta(meta_line) if meta_line.meta.id == self.thread_id => {
|
RolloutItem::SessionMeta(meta_line) if meta_line.meta.id == self.thread_id => {
|
||||||
update.created_at = parse_session_timestamp(meta_line.meta.timestamp.as_str());
|
update.created_at = parse_session_timestamp(meta_line.meta.timestamp.as_str());
|
||||||
update.source = Some(meta_line.meta.source.clone());
|
update.source = Some(meta_line.meta.source.clone());
|
||||||
|
if !meta_line.meta.originator.is_empty() {
|
||||||
|
update.originator = Some(meta_line.meta.originator.clone());
|
||||||
|
}
|
||||||
update.thread_source = Some(meta_line.meta.thread_source.clone());
|
update.thread_source = Some(meta_line.meta.thread_source.clone());
|
||||||
update.agent_nickname = Some(meta_line.meta.agent_nickname.clone());
|
update.agent_nickname = Some(meta_line.meta.agent_nickname.clone());
|
||||||
update.agent_role = Some(meta_line.meta.agent_role.clone());
|
update.agent_role = Some(meta_line.meta.agent_role.clone());
|
||||||
@@ -381,6 +385,7 @@ fn update_has_metadata_facts(update: &ThreadMetadataPatch) -> bool {
|
|||||||
|| update.created_at.is_some()
|
|| update.created_at.is_some()
|
||||||
|| update.advance_recency_at.is_some()
|
|| update.advance_recency_at.is_some()
|
||||||
|| update.source.is_some()
|
|| update.source.is_some()
|
||||||
|
|| update.originator.is_some()
|
||||||
|| update.thread_source.is_some()
|
|| update.thread_source.is_some()
|
||||||
|| update.agent_nickname.is_some()
|
|| update.agent_nickname.is_some()
|
||||||
|| update.agent_role.is_some()
|
|| update.agent_role.is_some()
|
||||||
@@ -436,7 +441,7 @@ mod tests {
|
|||||||
use crate::ThreadPersistenceMetadata;
|
use crate::ThreadPersistenceMetadata;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn create_without_project_omits_project_patch() {
|
async fn create_metadata_records_originator_without_project() {
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
let sync = ThreadMetadataSync::for_create(&CreateThreadParams {
|
let sync = ThreadMetadataSync::for_create(&CreateThreadParams {
|
||||||
session_id: thread_id.into(),
|
session_id: thread_id.into(),
|
||||||
@@ -464,7 +469,10 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let update = sync.take_pending_update().expect("pending metadata update");
|
let update = sync.take_pending_update().expect("pending metadata update");
|
||||||
assert_eq!(update.patch.project_id, None);
|
assert_eq!(
|
||||||
|
(update.patch.project_id, update.patch.originator.as_deref()),
|
||||||
|
(None, Some("test_originator")),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -488,6 +496,7 @@ mod tests {
|
|||||||
"2025-01-03T12:00:00+00:00"
|
"2025-01-03T12:00:00+00:00"
|
||||||
);
|
);
|
||||||
assert_eq!(update.patch.preview.as_deref(), Some("hello metadata"));
|
assert_eq!(update.patch.preview.as_deref(), Some("hello metadata"));
|
||||||
|
assert_eq!(update.patch.originator.as_deref(), Some("test_originator"));
|
||||||
assert_eq!(update.patch.title.as_deref(), Some("hello metadata"));
|
assert_eq!(update.patch.title.as_deref(), Some("hello metadata"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
update.patch.first_user_message.as_deref(),
|
update.patch.first_user_message.as_deref(),
|
||||||
@@ -817,6 +826,7 @@ mod tests {
|
|||||||
id: thread_id,
|
id: thread_id,
|
||||||
timestamp: "2025-01-03T12:00:00Z".to_string(),
|
timestamp: "2025-01-03T12:00:00Z".to_string(),
|
||||||
source: SessionSource::Exec,
|
source: SessionSource::Exec,
|
||||||
|
originator: "test_originator".to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
git: None,
|
git: None,
|
||||||
|
|||||||
@@ -569,6 +569,9 @@ pub struct ThreadOccurrenceSearchPage {
|
|||||||
/// Store-owned thread metadata used by list/read/resume responses.
|
/// Store-owned thread metadata used by list/read/resume responses.
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct StoredThread {
|
pub struct StoredThread {
|
||||||
|
/// Originator recorded at creation, if available from the backing store.
|
||||||
|
#[serde(default)]
|
||||||
|
pub originator: Option<String>,
|
||||||
/// Thread id.
|
/// Thread id.
|
||||||
pub thread_id: ThreadId,
|
pub thread_id: ThreadId,
|
||||||
/// Optional extra configuration fields for the thread.
|
/// Optional extra configuration fields for the thread.
|
||||||
@@ -687,7 +690,7 @@ impl GitInfoPatch {
|
|||||||
|
|
||||||
/// Patch for thread metadata.
|
/// Patch for thread metadata.
|
||||||
///
|
///
|
||||||
/// Every field is literal: `None` leaves that field unchanged, while `Some`
|
/// Unless noted otherwise, `None` leaves a field unchanged, while `Some`
|
||||||
/// applies the supplied value. Fields whose value may itself be cleared use an
|
/// applies the supplied value. Fields whose value may itself be cleared use an
|
||||||
/// inner `Option`, where `Some(None)` clears the field.
|
/// inner `Option`, where `Some(None)` clears the field.
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
@@ -724,6 +727,8 @@ pub struct ThreadMetadataPatch {
|
|||||||
pub advance_recency_at: Option<DateTime<Utc>>,
|
pub advance_recency_at: Option<DateTime<Utc>>,
|
||||||
/// Session source.
|
/// Session source.
|
||||||
pub source: Option<SessionSource>,
|
pub source: Option<SessionSource>,
|
||||||
|
/// Recorded creation-time originator. Only fills a missing stored value.
|
||||||
|
pub originator: Option<String>,
|
||||||
/// Optional analytics source classification.
|
/// Optional analytics source classification.
|
||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
@@ -817,6 +822,9 @@ impl ThreadMetadataPatch {
|
|||||||
if next.source.is_some() {
|
if next.source.is_some() {
|
||||||
self.source = next.source;
|
self.source = next.source;
|
||||||
}
|
}
|
||||||
|
if next.originator.is_some() {
|
||||||
|
self.originator = next.originator;
|
||||||
|
}
|
||||||
if next.thread_source.is_some() {
|
if next.thread_source.is_some() {
|
||||||
self.thread_source = next.thread_source;
|
self.thread_source = next.thread_source;
|
||||||
}
|
}
|
||||||
@@ -872,6 +880,7 @@ impl ThreadMetadataPatch {
|
|||||||
&& self.updated_at.is_none()
|
&& self.updated_at.is_none()
|
||||||
&& self.advance_recency_at.is_none()
|
&& self.advance_recency_at.is_none()
|
||||||
&& self.source.is_none()
|
&& self.source.is_none()
|
||||||
|
&& self.originator.is_none()
|
||||||
&& self.thread_source.is_none()
|
&& self.thread_source.is_none()
|
||||||
&& self.agent_nickname.is_none()
|
&& self.agent_nickname.is_none()
|
||||||
&& self.agent_role.is_none()
|
&& self.agent_role.is_none()
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ impl App {
|
|||||||
.request_typed::<ThreadListResponse>(ClientRequest::ThreadList {
|
.request_typed::<ThreadListResponse>(ClientRequest::ThreadList {
|
||||||
request_id: RequestId::String(Uuid::new_v4().to_string()),
|
request_id: RequestId::String(Uuid::new_v4().to_string()),
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor,
|
cursor,
|
||||||
limit: Some(AGENT_PICKER_PAGE_SIZE),
|
limit: Some(AGENT_PICKER_PAGE_SIZE),
|
||||||
sort_key: None,
|
sort_key: None,
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ fn overview_thread(
|
|||||||
status: ThreadStatus,
|
status: ThreadStatus,
|
||||||
) -> Thread {
|
) -> Thread {
|
||||||
Thread {
|
Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ impl App {
|
|||||||
.request_typed::<ThreadListResponse>(ClientRequest::ThreadList {
|
.request_typed::<ThreadListResponse>(ClientRequest::ThreadList {
|
||||||
request_id: RequestId::String(Uuid::new_v4().to_string()),
|
request_id: RequestId::String(Uuid::new_v4().to_string()),
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor,
|
cursor,
|
||||||
limit: Some(20),
|
limit: Some(20),
|
||||||
sort_key: Some(sort_key),
|
sort_key: Some(sort_key),
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ mod tests {
|
|||||||
|
|
||||||
fn test_thread(thread_id: ThreadId, source: SessionSource) -> Thread {
|
fn test_thread(thread_id: ThreadId, source: SessionSource) -> Thread {
|
||||||
Thread {
|
Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -4041,6 +4041,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
|
|||||||
agent_thread_id,
|
agent_thread_id,
|
||||||
ServerNotification::ThreadStarted(ThreadStartedNotification {
|
ServerNotification::ThreadStarted(ThreadStartedNotification {
|
||||||
thread: Thread {
|
thread: Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: agent_thread_id.to_string(),
|
id: agent_thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
@@ -4143,6 +4144,7 @@ async fn inactive_thread_started_notification_preserves_primary_model_when_path_
|
|||||||
agent_thread_id,
|
agent_thread_id,
|
||||||
ServerNotification::ThreadStarted(ThreadStartedNotification {
|
ServerNotification::ThreadStarted(ThreadStartedNotification {
|
||||||
thread: Thread {
|
thread: Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: agent_thread_id.to_string(),
|
id: agent_thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
@@ -4212,6 +4214,7 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() {
|
|||||||
app.primary_session_configured = Some(primary_session);
|
app.primary_session_configured = Some(primary_session);
|
||||||
|
|
||||||
let thread = Thread {
|
let thread = Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: read_thread_id.to_string(),
|
id: read_thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -1769,13 +1769,14 @@ async fn transcript_home_loads_every_older_history_page() -> Result<()> {
|
|||||||
app.chat_widget
|
app.chat_widget
|
||||||
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||||
app.handle_event(
|
// Keep the large dispatcher future off the Windows test thread's stack.
|
||||||
|
Box::pin(app.handle_event(
|
||||||
&mut tui,
|
&mut tui,
|
||||||
&mut app_server,
|
&mut app_server,
|
||||||
AppEvent::ExportTranscript {
|
AppEvent::ExportTranscript {
|
||||||
destination: TranscriptExportDestination::File(export_path.clone()),
|
destination: TranscriptExportDestination::File(export_path.clone()),
|
||||||
},
|
},
|
||||||
)
|
))
|
||||||
.await?;
|
.await?;
|
||||||
assert!(app.chat_widget.queued_user_message_texts().is_empty());
|
assert!(app.chat_widget.queued_user_message_texts().is_empty());
|
||||||
let markdown = std::fs::read_to_string(export_path)?;
|
let markdown = std::fs::read_to_string(export_path)?;
|
||||||
@@ -1813,7 +1814,7 @@ async fn transcript_home_loads_every_older_history_page() -> Result<()> {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| color_eyre::eyre::eyre!("history event channel closed"))?;
|
.ok_or_else(|| color_eyre::eyre::eyre!("history event channel closed"))?;
|
||||||
if matches!(event, AppEvent::OlderThreadHistoryLoaded { .. }) {
|
if matches!(event, AppEvent::OlderThreadHistoryLoaded { .. }) {
|
||||||
app.handle_event(&mut tui, &mut app_server, event).await?;
|
Box::pin(app.handle_event(&mut tui, &mut app_server, event)).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2234,7 +2235,8 @@ async fn underfilled_scrollback_fetches_older_pages_without_opening_the_transcri
|
|||||||
None => panic!("scrollback refill request channel closed"),
|
None => panic!("scrollback refill request channel closed"),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
app.handle_event(&mut tui, &mut app_server, request).await?;
|
// Keep the large dispatcher future off the Windows test thread's stack.
|
||||||
|
Box::pin(app.handle_event(&mut tui, &mut app_server, request)).await?;
|
||||||
let loaded = loop {
|
let loaded = loop {
|
||||||
match app_event_rx.recv().await {
|
match app_event_rx.recv().await {
|
||||||
Some(event @ AppEvent::OlderThreadHistoryLoaded { .. }) => break event,
|
Some(event @ AppEvent::OlderThreadHistoryLoaded { .. }) => break event,
|
||||||
@@ -2242,7 +2244,7 @@ async fn underfilled_scrollback_fetches_older_pages_without_opening_the_transcri
|
|||||||
None => panic!("older history page channel closed"),
|
None => panic!("older history page channel closed"),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
app.handle_event(&mut tui, &mut app_server, loaded).await?;
|
Box::pin(app.handle_event(&mut tui, &mut app_server, loaded)).await?;
|
||||||
|
|
||||||
assert!(app.overlay.is_none());
|
assert!(app.overlay.is_none());
|
||||||
assert!(app.transcript_cells.len() > initial_cell_count);
|
assert!(app.transcript_cells.len() > initial_cell_count);
|
||||||
|
|||||||
@@ -933,6 +933,7 @@ async fn known_thread_started_preserves_session_without_reading_unmaterialized_r
|
|||||||
);
|
);
|
||||||
let notification = ThreadStartedNotification {
|
let notification = ThreadStartedNotification {
|
||||||
thread: Thread {
|
thread: Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -410,6 +410,7 @@ mod tests {
|
|||||||
..test_thread_session(primary_thread_id, test_path_buf("/tmp/primary"))
|
..test_thread_session(primary_thread_id, test_path_buf("/tmp/primary"))
|
||||||
};
|
};
|
||||||
let read_thread = Thread {
|
let read_thread = Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: read_thread_id.to_string(),
|
id: read_thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -3695,6 +3695,7 @@ mod tests {
|
|||||||
let read_only_profile = PermissionProfile::read_only();
|
let read_only_profile = PermissionProfile::read_only();
|
||||||
let response = ThreadResumeResponse {
|
let response = ThreadResumeResponse {
|
||||||
thread: codex_app_server_protocol::Thread {
|
thread: codex_app_server_protocol::Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ async fn execute_inner(
|
|||||||
request(&handle, |request_id| ClientRequest::ThreadList {
|
request(&handle, |request_id| ClientRequest::ThreadList {
|
||||||
request_id,
|
request_id,
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: arguments.cursor.clone(),
|
cursor: arguments.cursor.clone(),
|
||||||
limit: Some(limit),
|
limit: Some(limit),
|
||||||
sort_key: Some(ThreadSortKey::UpdatedAt),
|
sort_key: Some(ThreadSortKey::UpdatedAt),
|
||||||
|
|||||||
@@ -755,6 +755,7 @@ fn latest_session_lookup_params(
|
|||||||
lookup_mode: LatestSessionLookupMode,
|
lookup_mode: LatestSessionLookupMode,
|
||||||
) -> ThreadListParams {
|
) -> ThreadListParams {
|
||||||
ThreadListParams {
|
ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(1),
|
limit: Some(1),
|
||||||
sort_key: Some(AppServerThreadSortKey::UpdatedAt),
|
sort_key: Some(AppServerThreadSortKey::UpdatedAt),
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ impl<'a> NamedSessionCandidates<'a> {
|
|||||||
};
|
};
|
||||||
let response = app_server
|
let response = app_server
|
||||||
.thread_list(ThreadListParams {
|
.thread_list(ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: self.cursor.clone(),
|
cursor: self.cursor.clone(),
|
||||||
limit: Some(100),
|
limit: Some(100),
|
||||||
sort_key: Some(sort_key),
|
sort_key: Some(sort_key),
|
||||||
|
|||||||
@@ -2003,6 +2003,7 @@ fn thread_list_params(
|
|||||||
use_state_db_only: bool,
|
use_state_db_only: bool,
|
||||||
) -> ThreadListParams {
|
) -> ThreadListParams {
|
||||||
ThreadListParams {
|
ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor,
|
cursor,
|
||||||
limit: Some(PAGE_SIZE as u32),
|
limit: Some(PAGE_SIZE as u32),
|
||||||
sort_key: Some(sort_key),
|
sort_key: Some(sort_key),
|
||||||
@@ -6266,6 +6267,7 @@ session_picker_view = "dense"
|
|||||||
fn app_server_row_keeps_pathless_threads() {
|
fn app_server_row_keeps_pathless_threads() {
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
let thread = Thread {
|
let thread = Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
@@ -6311,6 +6313,7 @@ session_picker_view = "dense"
|
|||||||
|
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
let thread = Thread {
|
let thread = Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
@@ -6396,6 +6399,7 @@ session_picker_view = "dense"
|
|||||||
|
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
let thread = Thread {
|
let thread = Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
@@ -6472,6 +6476,7 @@ session_picker_view = "dense"
|
|||||||
|
|
||||||
let thread_id = ThreadId::new();
|
let thread_id = ThreadId::new();
|
||||||
let thread = Thread {
|
let thread = Thread {
|
||||||
|
originator: None,
|
||||||
environments: None,
|
environments: None,
|
||||||
id: thread_id.to_string(),
|
id: thread_id.to_string(),
|
||||||
extra: None,
|
extra: None,
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ pub(crate) fn spawn_search(
|
|||||||
"task-title-search-{request_generation}"
|
"task-title-search-{request_generation}"
|
||||||
)),
|
)),
|
||||||
params: ThreadListParams {
|
params: ThreadListParams {
|
||||||
|
originators: None,
|
||||||
cursor: None,
|
cursor: None,
|
||||||
limit: Some(MAX_SEARCH_RESULTS),
|
limit: Some(MAX_SEARCH_RESULTS),
|
||||||
sort_key: Some(ThreadSortKey::UpdatedAt),
|
sort_key: Some(ThreadSortKey::UpdatedAt),
|
||||||
|
|||||||
Reference in new issue
Block a user