mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Add persisted manual ordering for thread sections (#36007)
## What changed - Add `thread/section/move` to atomically move a thread into, within, or out of a section. Threads can be inserted before an existing member or appended, and moves within a section preserve `sectionEnteredAt`. - Add `section_position` sorting to `thread/list`, with ascending order as its default, and expose `sectionEnteredAt` in thread responses. - Persist section positions and entry times in SQLite, including migration of existing section members into recency order. Section membership is no longer updated through `thread/metadata/update`. ## Testing - Cover section moves, reordering, pagination, persistence across restarts and rollout reconciliation, concurrent updates, and rank renumbering. GitOrigin-RevId: aec6d7ddedca5277029b5caf5c074975397e956c
This commit is contained in:
1 parent
c41a38dd10
commit
ad6fc66b6d
75 files changed
+2879
-543
No files matched your search
@@ -25,7 +25,10 @@ pub enum Account {
|
||||
#[serde(rename = "chatgpt", rename_all = "camelCase")]
|
||||
#[ts(rename = "chatgpt", rename_all = "camelCase")]
|
||||
Chatgpt {
|
||||
#[schemars(required, schema_with = "nullable_string_schema")]
|
||||
#[schemars(
|
||||
required,
|
||||
schema_with = "crate::protocol::serde_helpers::nullable_string_schema"
|
||||
)]
|
||||
email: Option<String>,
|
||||
plan_type: PlanType,
|
||||
},
|
||||
@@ -38,12 +41,6 @@ pub enum Account {
|
||||
},
|
||||
}
|
||||
|
||||
fn nullable_string_schema(
|
||||
generator: &mut schemars::r#gen::SchemaGenerator,
|
||||
) -> schemars::schema::Schema {
|
||||
generator.subschema_for::<Option<String>>()
|
||||
}
|
||||
|
||||
impl From<ProviderAccount> for Account {
|
||||
fn from(account: ProviderAccount) -> Self {
|
||||
match account {
|
||||
|
||||
@@ -240,6 +240,7 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
||||
id: "01984de2-8f74-7c91-a3b2-5c5e937cf318".to_string(),
|
||||
name: "Pinned".to_string(),
|
||||
}),
|
||||
section_entered_at: Some(1),
|
||||
history_mode: Default::default(),
|
||||
model_provider: "openai".to_string(),
|
||||
created_at: 1,
|
||||
@@ -287,15 +288,18 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
||||
"name": "Pinned",
|
||||
})
|
||||
);
|
||||
assert_eq!(value["thread"]["sectionEnteredAt"], json!(1));
|
||||
|
||||
let mut legacy_thread = value["thread"].clone();
|
||||
legacy_thread
|
||||
let legacy_thread_fields = legacy_thread
|
||||
.as_object_mut()
|
||||
.expect("serialized thread should be an object")
|
||||
.remove("section");
|
||||
.expect("serialized thread should be an object");
|
||||
legacy_thread_fields.remove("section");
|
||||
legacy_thread_fields.remove("sectionEnteredAt");
|
||||
let legacy_thread =
|
||||
serde_json::from_value::<Thread>(legacy_thread).expect("deserialize legacy thread");
|
||||
assert_eq!(legacy_thread.section, None);
|
||||
assert_eq!(legacy_thread.section_entered_at, None);
|
||||
|
||||
assert_eq!(
|
||||
value.get("initialTurnsPage"),
|
||||
@@ -474,39 +478,6 @@ fn thread_list_params_accepts_section_id_filter() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_metadata_update_params_distinguish_section_id_set_clear_and_omission() {
|
||||
for section_id in [
|
||||
"01984de2-8f74-7c91-a3b2-5c5e937cf318",
|
||||
"01984de2-8f74-7c91-a3b2-5c5e937cf319",
|
||||
] {
|
||||
let params = serde_json::from_value::<ThreadMetadataUpdateParams>(json!({
|
||||
"threadId": "thr_123",
|
||||
"sectionId": section_id,
|
||||
}))
|
||||
.expect("section ID metadata patch should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
params.section_id.as_ref().map(|section| section.as_deref()),
|
||||
Some(Some(section_id))
|
||||
);
|
||||
assert_eq!(params.git_info, None);
|
||||
}
|
||||
|
||||
let params = serde_json::from_value::<ThreadMetadataUpdateParams>(json!({
|
||||
"threadId": "thr_123",
|
||||
"sectionId": null,
|
||||
}))
|
||||
.expect("cleared section ID metadata patch should deserialize");
|
||||
assert_eq!(params.section_id, Some(None));
|
||||
|
||||
let params = serde_json::from_value::<ThreadMetadataUpdateParams>(json!({
|
||||
"threadId": "thr_123",
|
||||
}))
|
||||
.expect("omitted section ID metadata patch should deserialize");
|
||||
assert_eq!(params.section_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_section_list_params_and_response_round_trip() {
|
||||
let params = serde_json::from_value::<ThreadSectionListParams>(json!({
|
||||
|
||||
@@ -866,15 +866,6 @@ pub struct ThreadMetadataUpdateParams {
|
||||
/// provide a string to replace the stored value.
|
||||
#[ts(optional = nullable)]
|
||||
pub git_info: Option<ThreadMetadataGitInfoUpdateParams>,
|
||||
/// Omit to leave the section unchanged, set to `null` to clear it, or provide a section ID.
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "crate::protocol::serde_helpers::serialize_double_option",
|
||||
deserialize_with = "crate::protocol::serde_helpers::deserialize_double_option"
|
||||
)]
|
||||
#[ts(optional = nullable, type = "string | null")]
|
||||
pub section_id: Option<Option<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
@@ -920,6 +911,31 @@ pub struct ThreadMetadataUpdateResponse {
|
||||
pub thread: Thread,
|
||||
}
|
||||
|
||||
/// Parameters for moving a thread within a server-owned section ordering.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadSectionMoveParams {
|
||||
/// Thread to move into, within, or out of a section.
|
||||
pub thread_id: String,
|
||||
/// Destination section, or `null` to remove the thread from its section.
|
||||
#[serde(deserialize_with = "Option::deserialize")]
|
||||
#[schemars(
|
||||
required,
|
||||
schema_with = "crate::protocol::serde_helpers::nullable_string_schema"
|
||||
)]
|
||||
#[ts(type = "string | null")]
|
||||
pub section_id: Option<String>,
|
||||
/// Existing thread to insert before; omission or null appends to the section.
|
||||
#[ts(optional = nullable)]
|
||||
pub before_thread_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadSectionMoveResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(rename_all = "lowercase")]
|
||||
@@ -1196,7 +1212,7 @@ pub struct ThreadSearchParams {
|
||||
pub limit: Option<u32>,
|
||||
/// Optional sort key; defaults to created_at.
|
||||
#[ts(optional = nullable)]
|
||||
pub sort_key: Option<ThreadSortKey>,
|
||||
pub sort_key: Option<ThreadSearchSortKey>,
|
||||
/// Optional sort direction; defaults to descending (newest first).
|
||||
#[ts(optional = nullable)]
|
||||
pub sort_direction: Option<SortDirection>,
|
||||
@@ -1244,6 +1260,16 @@ pub enum ThreadSortKey {
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
RecencyAt,
|
||||
SectionPosition,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ThreadSearchSortKey {
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
RecencyAt,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, JsonSchema, TS)]
|
||||
|
||||
@@ -197,6 +197,10 @@ pub struct Thread {
|
||||
/// The independently persisted section selected for this thread, if any.
|
||||
#[serde(default)]
|
||||
pub section: Option<ThreadSection>,
|
||||
/// Unix timestamp in seconds when the thread entered its current section.
|
||||
#[serde(default)]
|
||||
#[ts(type = "number | null")]
|
||||
pub section_entered_at: Option<i64>,
|
||||
/// Persisted thread history contract selected when this thread was created.
|
||||
#[experimental("thread.historyMode")]
|
||||
#[serde(default)]
|
||||
|
||||
Reference in new issue
Block a user