feat: Set chat name (#4974)
Set chat name with `/name` so they appear in the codex resume page: https://github.com/user-attachments/assets/c0252bba-3a53-44c7-a740-f4690a3ad405
This commit is contained in:
@@ -88,6 +88,7 @@ use crate::protocol::ReviewDecision;
|
||||
use crate::protocol::ReviewOutputEvent;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::protocol::SessionConfiguredEvent;
|
||||
use crate::protocol::SessionRenamedEvent;
|
||||
use crate::protocol::StreamErrorEvent;
|
||||
use crate::protocol::Submission;
|
||||
use crate::protocol::TokenCountEvent;
|
||||
@@ -1507,6 +1508,16 @@ async fn submission_loop(
|
||||
};
|
||||
sess.send_event(event).await;
|
||||
}
|
||||
Op::SetSessionName { name } => {
|
||||
// Persist a rename event and notify the client. We rely on the
|
||||
// recorder's filtering to include this in the rollout.
|
||||
let sub_id = sub.id.clone();
|
||||
let event = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::SessionRenamed(SessionRenamedEvent { name }),
|
||||
};
|
||||
sess.send_event(event).await;
|
||||
}
|
||||
Op::Review { review_request } => {
|
||||
spawn_review_thread(
|
||||
sess.clone(),
|
||||
|
||||
@@ -17,6 +17,7 @@ use super::SESSIONS_SUBDIR;
|
||||
use crate::protocol::EventMsg;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
use codex_protocol::protocol::SessionRenamedEvent;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
|
||||
/// Returned page of conversation summaries.
|
||||
@@ -41,6 +42,8 @@ pub struct ConversationItem {
|
||||
pub head: Vec<serde_json::Value>,
|
||||
/// Last up to `TAIL_RECORD_LIMIT` JSONL response records parsed as JSON.
|
||||
pub tail: Vec<serde_json::Value>,
|
||||
/// Latest human-friendly session name, if any.
|
||||
pub name: Option<String>,
|
||||
/// RFC3339 timestamp string for when the session was created, if available.
|
||||
pub created_at: Option<String>,
|
||||
/// RFC3339 timestamp string for the most recent response in the tail, if available.
|
||||
@@ -56,6 +59,7 @@ struct HeadTailSummary {
|
||||
source: Option<SessionSource>,
|
||||
created_at: Option<String>,
|
||||
updated_at: Option<String>,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
/// Hard cap to bound worst‑case work per request.
|
||||
@@ -222,6 +226,7 @@ async fn traverse_directories_for_paths(
|
||||
path,
|
||||
head,
|
||||
tail,
|
||||
name: summary.name,
|
||||
created_at,
|
||||
updated_at,
|
||||
});
|
||||
@@ -382,14 +387,21 @@ async fn read_head_and_tail(
|
||||
if matches!(ev, EventMsg::UserMessage(_)) {
|
||||
summary.saw_user_event = true;
|
||||
}
|
||||
if let EventMsg::SessionRenamed(SessionRenamedEvent { name }) = ev {
|
||||
summary.name = Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tail_limit != 0 {
|
||||
let (tail, updated_at) = read_tail_records(path, tail_limit).await?;
|
||||
let (tail, updated_at, latest_name) = read_tail_records(path, tail_limit).await?;
|
||||
summary.tail = tail;
|
||||
summary.updated_at = updated_at;
|
||||
// Prefer the most recent rename event discovered from the tail scan; fallback to any name seen in head.
|
||||
if latest_name.is_some() {
|
||||
summary.name = latest_name;
|
||||
}
|
||||
}
|
||||
Ok(summary)
|
||||
}
|
||||
@@ -397,13 +409,13 @@ async fn read_head_and_tail(
|
||||
async fn read_tail_records(
|
||||
path: &Path,
|
||||
max_records: usize,
|
||||
) -> io::Result<(Vec<serde_json::Value>, Option<String>)> {
|
||||
) -> io::Result<(Vec<serde_json::Value>, Option<String>, Option<String>)> {
|
||||
use std::io::SeekFrom;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncSeekExt;
|
||||
|
||||
if max_records == 0 {
|
||||
return Ok((Vec::new(), None));
|
||||
return Ok((Vec::new(), None, None));
|
||||
}
|
||||
|
||||
const CHUNK_SIZE: usize = 8192;
|
||||
@@ -411,28 +423,33 @@ async fn read_tail_records(
|
||||
let mut file = tokio::fs::File::open(path).await?;
|
||||
let mut pos = file.seek(SeekFrom::End(0)).await?;
|
||||
if pos == 0 {
|
||||
return Ok((Vec::new(), None));
|
||||
return Ok((Vec::new(), None, None));
|
||||
}
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
let mut latest_timestamp: Option<String> = None;
|
||||
let mut latest_name: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let slice_start = match (pos > 0, buffer.iter().position(|&b| b == b'\n')) {
|
||||
(true, Some(idx)) => idx + 1,
|
||||
_ => 0,
|
||||
};
|
||||
let (tail, newest_ts) = collect_last_response_values(&buffer[slice_start..], max_records);
|
||||
let (tail, newest_ts, name_opt) =
|
||||
collect_last_response_values(&buffer[slice_start..], max_records);
|
||||
if latest_timestamp.is_none() {
|
||||
latest_timestamp = newest_ts.clone();
|
||||
}
|
||||
if latest_name.is_none() {
|
||||
latest_name = name_opt.clone();
|
||||
}
|
||||
if tail.len() >= max_records || pos == 0 {
|
||||
return Ok((tail, latest_timestamp.or(newest_ts)));
|
||||
return Ok((tail, latest_timestamp.or(newest_ts), latest_name));
|
||||
}
|
||||
|
||||
let read_size = CHUNK_SIZE.min(pos as usize);
|
||||
if read_size == 0 {
|
||||
return Ok((tail, latest_timestamp.or(newest_ts)));
|
||||
return Ok((tail, latest_timestamp.or(newest_ts), latest_name));
|
||||
}
|
||||
pos -= read_size as u64;
|
||||
file.seek(SeekFrom::Start(pos)).await?;
|
||||
@@ -446,16 +463,17 @@ async fn read_tail_records(
|
||||
fn collect_last_response_values(
|
||||
buffer: &[u8],
|
||||
max_records: usize,
|
||||
) -> (Vec<serde_json::Value>, Option<String>) {
|
||||
) -> (Vec<serde_json::Value>, Option<String>, Option<String>) {
|
||||
use std::borrow::Cow;
|
||||
|
||||
if buffer.is_empty() || max_records == 0 {
|
||||
return (Vec::new(), None);
|
||||
return (Vec::new(), None, None);
|
||||
}
|
||||
|
||||
let text: Cow<'_, str> = String::from_utf8_lossy(buffer);
|
||||
let mut collected_rev: Vec<serde_json::Value> = Vec::new();
|
||||
let mut latest_timestamp: Option<String> = None;
|
||||
let mut latest_name: Option<String> = None;
|
||||
for line in text.lines().rev() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -464,20 +482,30 @@ fn collect_last_response_values(
|
||||
let parsed: serde_json::Result<RolloutLine> = serde_json::from_str(trimmed);
|
||||
let Ok(rollout_line) = parsed else { continue };
|
||||
let RolloutLine { timestamp, item } = rollout_line;
|
||||
if let RolloutItem::ResponseItem(item) = item
|
||||
&& let Ok(val) = serde_json::to_value(&item)
|
||||
{
|
||||
if latest_timestamp.is_none() {
|
||||
latest_timestamp = Some(timestamp.clone());
|
||||
match item {
|
||||
RolloutItem::ResponseItem(item) => {
|
||||
if let Ok(val) = serde_json::to_value(&item) {
|
||||
if latest_timestamp.is_none() {
|
||||
latest_timestamp = Some(timestamp.clone());
|
||||
}
|
||||
collected_rev.push(val);
|
||||
if collected_rev.len() == max_records {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
collected_rev.push(val);
|
||||
if collected_rev.len() == max_records {
|
||||
break;
|
||||
RolloutItem::EventMsg(ev) => {
|
||||
if latest_name.is_none()
|
||||
&& let EventMsg::SessionRenamed(SessionRenamedEvent { name }) = ev
|
||||
{
|
||||
latest_name = Some(name);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
collected_rev.reverse();
|
||||
(collected_rev, latest_timestamp)
|
||||
(collected_rev, latest_timestamp, latest_name)
|
||||
}
|
||||
|
||||
/// Locate a recorded conversation rollout file by its UUID string using the existing
|
||||
|
||||
@@ -39,6 +39,7 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool {
|
||||
| EventMsg::AgentMessage(_)
|
||||
| EventMsg::AgentReasoning(_)
|
||||
| EventMsg::AgentReasoningRawContent(_)
|
||||
| EventMsg::SessionRenamed(_)
|
||||
| EventMsg::TokenCount(_)
|
||||
| EventMsg::EnteredReviewMode(_)
|
||||
| EventMsg::ExitedReviewMode(_)
|
||||
|
||||
@@ -196,6 +196,7 @@ async fn test_list_conversations_latest_first() {
|
||||
path: p1,
|
||||
head: head_3,
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some("2025-01-03T12-00-00".into()),
|
||||
updated_at: Some("2025-01-03T12-00-00".into()),
|
||||
},
|
||||
@@ -203,6 +204,7 @@ async fn test_list_conversations_latest_first() {
|
||||
path: p2,
|
||||
head: head_2,
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some("2025-01-02T12-00-00".into()),
|
||||
updated_at: Some("2025-01-02T12-00-00".into()),
|
||||
},
|
||||
@@ -210,6 +212,7 @@ async fn test_list_conversations_latest_first() {
|
||||
path: p3,
|
||||
head: head_1,
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some("2025-01-01T12-00-00".into()),
|
||||
updated_at: Some("2025-01-01T12-00-00".into()),
|
||||
},
|
||||
@@ -317,6 +320,7 @@ async fn test_pagination_cursor() {
|
||||
path: p5,
|
||||
head: head_5,
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some("2025-03-05T09-00-00".into()),
|
||||
updated_at: Some("2025-03-05T09-00-00".into()),
|
||||
},
|
||||
@@ -324,6 +328,7 @@ async fn test_pagination_cursor() {
|
||||
path: p4,
|
||||
head: head_4,
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some("2025-03-04T09-00-00".into()),
|
||||
updated_at: Some("2025-03-04T09-00-00".into()),
|
||||
},
|
||||
@@ -380,6 +385,7 @@ async fn test_pagination_cursor() {
|
||||
path: p3,
|
||||
head: head_3,
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some("2025-03-03T09-00-00".into()),
|
||||
updated_at: Some("2025-03-03T09-00-00".into()),
|
||||
},
|
||||
@@ -387,6 +393,7 @@ async fn test_pagination_cursor() {
|
||||
path: p2,
|
||||
head: head_2,
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some("2025-03-02T09-00-00".into()),
|
||||
updated_at: Some("2025-03-02T09-00-00".into()),
|
||||
},
|
||||
@@ -427,6 +434,7 @@ async fn test_pagination_cursor() {
|
||||
path: p1,
|
||||
head: head_1,
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some("2025-03-01T09-00-00".into()),
|
||||
updated_at: Some("2025-03-01T09-00-00".into()),
|
||||
}],
|
||||
@@ -475,6 +483,7 @@ async fn test_get_conversation_contents() {
|
||||
path: expected_path,
|
||||
head: expected_head,
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some(ts.into()),
|
||||
updated_at: Some(ts.into()),
|
||||
}],
|
||||
@@ -823,6 +832,7 @@ async fn test_stable_ordering_same_second_pagination() {
|
||||
path: p3,
|
||||
head: head(u3),
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some(ts.to_string()),
|
||||
updated_at: Some(ts.to_string()),
|
||||
},
|
||||
@@ -830,6 +840,7 @@ async fn test_stable_ordering_same_second_pagination() {
|
||||
path: p2,
|
||||
head: head(u2),
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some(ts.to_string()),
|
||||
updated_at: Some(ts.to_string()),
|
||||
},
|
||||
@@ -860,6 +871,7 @@ async fn test_stable_ordering_same_second_pagination() {
|
||||
path: p1,
|
||||
head: head(u1),
|
||||
tail: Vec::new(),
|
||||
name: None,
|
||||
created_at: Some(ts.to_string()),
|
||||
updated_at: Some(ts.to_string()),
|
||||
}],
|
||||
|
||||
Reference in New Issue
Block a user