[codex] Add user input client ids (#24653)

## Summary

Adds an optional `clientId` field to app-server v2 `UserInput` and
carries it through the core `UserInput` model so clients can correlate
echoed user input items without relying on payload equality.

## Details

- Adds `client_id: Option<String>` to core `UserInput` variants.
- Exposes the v2 app-server field as `clientId` on the wire and in
generated TypeScript.
- Preserves the id when converting between app-server v2 and core
protocol types.
- Regenerates app-server schema fixtures.

## Validation

- `just fmt`
- `just write-app-server-schema`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-protocol`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-protocol`
- `git diff --check`
This commit is contained in:
Alexi Christakis
2026-05-28 14:54:39 -07:00
committed by GitHub
parent a027135bc6
commit e92c952b2e
113 changed files with 793 additions and 97 deletions
+6
View File
@@ -56,6 +56,9 @@ pub enum TurnItem {
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
pub struct UserMessageItem {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub client_id: Option<String>,
pub content: Vec<UserInput>,
}
@@ -237,6 +240,7 @@ impl UserMessageItem {
pub fn new(content: &[UserInput]) -> Self {
Self {
id: uuid::Uuid::new_v4().to_string(),
client_id: None,
content: content.to_vec(),
}
}
@@ -245,6 +249,7 @@ impl UserMessageItem {
// Legacy user-message events flatten only text inputs into `message` and
// rebase text element ranges onto that concatenated text.
EventMsg::UserMessage(UserMessageEvent {
client_id: self.client_id.clone(),
message: self.message(),
images: Some(self.image_urls()),
image_details: self.image_details(),
@@ -272,6 +277,7 @@ impl UserMessageItem {
if let UserInput::Text {
text,
text_elements,
..
} = input
{
// Text element ranges are relative to each text chunk; offset them so they align
+4 -2
View File
@@ -1237,7 +1237,9 @@ impl From<Vec<UserInput>> for ResponseInputItem {
.into_iter()
.flat_map(|c| match c {
UserInput::Text { text, .. } => vec![ContentItem::InputText { text }],
UserInput::Image { image_url, detail } => {
UserInput::Image {
image_url, detail, ..
} => {
image_index += 1;
let detail = detail.unwrap_or(DEFAULT_IMAGE_DETAIL);
vec![ContentItem::InputImage {
@@ -1245,7 +1247,7 @@ impl From<Vec<UserInput>> for ResponseInputItem {
detail: Some(detail),
}]
}
UserInput::LocalImage { path, detail } => {
UserInput::LocalImage { path, detail, .. } => {
image_index += 1;
let detail = detail.unwrap_or(DEFAULT_IMAGE_DETAIL);
match std::fs::read(&path) {
+9 -1
View File
@@ -129,6 +129,9 @@ pub struct Submission {
pub id: String,
/// Payload
pub op: Op,
/// Client-provided id for the user message represented by `Op::UserInput`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_user_message_id: Option<String>,
/// Optional W3C trace carrier propagated across async submission handoffs.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trace: Option<W3cTraceContext>,
@@ -2164,6 +2167,8 @@ pub struct AgentMessageEvent {
#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema, TS)]
pub struct UserMessageEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
pub message: String,
/// Image URLs sourced from `UserInput::Image`. These are safe
/// to replay in legacy UI history events and correspond to images sent to
@@ -5088,6 +5093,7 @@ mod tests {
#[test]
fn user_message_event_serializes_empty_metadata_vectors() -> Result<()> {
let event = UserMessageEvent {
client_id: None,
message: "hello".to_string(),
images: None,
local_images: Vec::new(),
@@ -5133,7 +5139,7 @@ mod tests {
#[test]
fn user_message_item_legacy_event_preserves_image_details() {
let local_path = PathBuf::from("/tmp/local.png");
let item = UserMessageItem::new(&[
let mut item = UserMessageItem::new(&[
crate::user_input::UserInput::Image {
image_url: "https://example.com/first.png".to_string(),
detail: Some(ImageDetail::Original),
@@ -5147,6 +5153,7 @@ mod tests {
detail: Some(ImageDetail::Original),
},
]);
item.client_id = Some("client-message-1".to_string());
let EventMsg::UserMessage(event) = item.as_legacy_event() else {
panic!("expected user message event");
@@ -5159,6 +5166,7 @@ mod tests {
"https://example.com/second.png".to_string(),
])
);
assert_eq!(event.client_id, Some("client-message-1".to_string()));
assert_eq!(event.image_details, vec![Some(ImageDetail::Original)]);
assert_eq!(event.local_images, vec![local_path]);
assert_eq!(event.local_image_details, vec![Some(ImageDetail::Original)]);