Skip to content

[codex] Add user input client ids#24653

Merged
alexi-openai merged 6 commits into
mainfrom
codex/user-input-client-id
May 28, 2026
Merged

[codex] Add user input client ids#24653
alexi-openai merged 6 commits into
mainfrom
codex/user-input-client-id

Conversation

@alexi-openai
Copy link
Copy Markdown
Contributor

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@alexi-openai alexi-openai force-pushed the codex/user-input-client-id branch from 9fc5e62 to 3e25979 Compare May 26, 2026 23:07
pub enum UserInput {
Text {
#[serde(rename = "clientId", default, skip_serializing_if = "Option::is_none")]
#[ts(rename = "clientId", optional)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove the skip_serializing_if = "Option::is_none" part for types exposed over app-server

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

everything else about the annotations look good though

Comment thread codex-rs/protocol/src/user_input.rs Outdated
#[serde(tag = "type", rename_all = "snake_case")]
pub enum UserInput {
Text {
#[serde(default, skip_serializing_if = "Option::is_none")]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks good in core protocol 👍

@alexi-openai alexi-openai marked this pull request as ready for review May 26, 2026 23:50
@alexi-openai alexi-openai force-pushed the codex/user-input-client-id branch from 3e25979 to f71bb24 Compare May 27, 2026 00:02
@alexi-openai
Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request May 27, 2026
Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3e25979d31

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/protocol/src/user_input.rs Outdated
Comment on lines +17 to +18
#[serde(default, skip_serializing_if = "Option::is_none")]
client_id: Option<String>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore build compatibility for UserInput literals

Adding client_id as a required Rust field on the UserInput variants breaks existing call sites that this commit does not update; for example, codex-rs/cli/src/main.rs:1701 still constructs UserInput::LocalImage { path, detail: None } and :1704 constructs UserInput::Text without client_id, so crates using those literals will fail to compile. Either update all constructors or introduce a constructor/defaulted API that keeps call sites source-compatible.

Useful? React with 👍 / 👎.

Comment on lines +1071 to +1072
content.push(UserInput::Text {
client_id: None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve client IDs when replaying user messages

When a v2 turn includes clientId, the live ItemCompleted(UserMessage) carries it, but persisted history replay still ignores user-message item-completed events and rebuilds the user message from the legacy UserMessageEvent; hard-coding None here means thread/read, resume, or refresh drops every client ID after replay. Clients relying on these IDs to reconcile their submitted input items will only see them on the live notification path, so replay should use the structured item or persist the IDs in the legacy event.

Useful? React with 👍 / 👎.

Comment on lines +269 to +270
#[serde(rename = "clientId", default, skip_serializing_if = "Option::is_none")]
#[ts(rename = "clientId", optional)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep v2 clientId fields nullable on the wire

The app-server v2 guidance in AGENTS.md says not to use skip_serializing_if = "Option::is_none" for v2 payload fields; with this annotation, any UserInput returned in ItemStarted/ItemCompleted or thread responses will omit clientId when it is absent instead of serializing the stable nullable field shape used by the rest of the v2 API. Please model this as a nullable field on the wire (and align the TS annotation for request typing) rather than making it disappear.

Useful? React with 👍 / 👎.

Comment on lines +1071 to +1072
content.push(UserInput::Text {
client_id: None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve client IDs in live turn snapshots

For live app-server threads, ThreadState::track_current_turn_event also rebuilds the active turn through ThreadHistoryBuilder: the structured ItemCompleted(UserMessage) now has the client_ids, but the builder ignores that item type and later records the legacy UserMessageEvent, which lands here with None. As a result TurnCompletedNotification and other active-turn snapshots drop the client IDs even before any persisted replay, so clients that reconcile from the turn snapshot will lose the IDs despite receiving them on the individual item notification.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

@owenlin0 owenlin0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, i realized this approach got quite invasive. can we try something like this instead?

// input
TurnStartParams { client_user_message_id: Option<String>, input: Vec<UserInput>, ... }
TurnSteerParams { client_user_message_id: Option<String>, input: Vec<UserInput>, ... }

// output
ThreadItem::UserMessage {
    id: String,
    client_id: Option<String>,
    content: Vec<UserInput>,
}

Copy link
Copy Markdown
Collaborator

owenlin0 commented May 28, 2026

I think the message-level ID is the right one, but there's some funky merging logic going on - we can improve with the fix below:

The cleanest version is to persist the same message-level id on the already-persisted UserMessageEvent, then rebuild ThreadItem::UserMessage.clientId directly from that event during thread/read / thread/resume:

  • keep turn/start.clientUserMessageId and turn/steer.clientUserMessageId
  • keep UserMessageItem.client_id for live item/* notifications
  • add optional client_id to UserMessageEvent
  • copy UserMessageItem.client_id into UserMessageEvent in as_legacy_event()
  • have ThreadHistoryBuilder::handle_user_message() set client_id: payload.client_id.clone()
  • remove merge_user_message_client_id / set_user_message_client_id and ignore user-message ItemStarted / ItemCompleted again

That would fix both problems with the current patch: persisted thread/read / thread/resume would retain the id because UserMessageEvent is already persisted, and running-thread history would not risk duplicating a user message when ItemStarted(UserMessage) arrives before the legacy UserMessage event.

pub struct UserMessageEvent {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub client_id: Option<String>,
    ...
}

}

#[test]
fn user_message_client_id_uses_camel_case_wire_field() {
Copy link
Copy Markdown
Collaborator

@owenlin0 owenlin0 May 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this kind of test is low value IMO, we can remove

UserMessage { id: String, content: Vec<UserInput> },
UserMessage {
id: String,
#[serde(default)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's remove the serde(default) annotation here, not needed

@alexi-openai alexi-openai force-pushed the codex/user-input-client-id branch from e6eb304 to 6bb0032 Compare May 28, 2026 17:58
@alexi-openai alexi-openai requested a review from owenlin0 May 28, 2026 20:30
@alexi-openai alexi-openai merged commit e92c952 into main May 28, 2026
31 checks passed
@alexi-openai alexi-openai deleted the codex/user-input-client-id branch May 28, 2026 21:54
@github-actions github-actions Bot locked and limited conversation to collaborators May 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants