Skip to content

Commit be13f03

Browse files
ningyi-oaicodex
andauthored
Pass turn id with feedback uploads (openai#17314)
## Summary - Add an optional `tags` dictionary to feedback upload params. - Capture the active app-server turn id in the TUI and submit it as `tags.turn_id` with `/feedback` uploads. - Merge client-provided feedback tags into Sentry feedback tags while preserving reserved system fields like `thread_id`, `classification`, `cli_version`, `session_source`, and `reason`. ## Behavior / impact Existing feedback upload callers remain compatible because `tags` is optional and nullable. The wire shape is still a normal JSON object / TypeScript dictionary, so adding future feedback metadata will not require a new top-level protocol field each time. This change only adds feedback metadata for Codex CLI/TUI uploads; it does not affect existing pipelines, DAGs, exports, or downstream consumers unless they choose to read the new `turn_id` feedback tag. ## Tests - `cargo fmt -- --config imports_granularity=Item` passed; stable rustfmt warned that `imports_granularity` is nightly-only. - `cargo run -p codex-app-server-protocol --bin write_schema_fixtures` - `cargo test -p codex-feedback upload_tags_include_client_tags_and_preserve_reserved_fields` - `cargo test -p codex-app-server-protocol schema_fixtures_match_generated` - `cargo test -p codex-tui build_feedback_upload_params` - `cargo test -p codex-tui live_app_server_turn_started_sets_feedback_turn_id` - `cargo check -p codex-app-server --tests` - `git diff --check` --------- Co-authored-by: Codex <noreply@openai.com>
1 parent dbfe855 commit be13f03

14 files changed

Lines changed: 284 additions & 59 deletions

File tree

codex-rs/app-server-protocol/schema/json/ClientRequest.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,15 @@
647647
"null"
648648
]
649649
},
650+
"tags": {
651+
"additionalProperties": {
652+
"type": "string"
653+
},
654+
"type": [
655+
"object",
656+
"null"
657+
]
658+
},
650659
"threadId": {
651660
"type": [
652661
"string",

codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7479,6 +7479,15 @@
74797479
"null"
74807480
]
74817481
},
7482+
"tags": {
7483+
"additionalProperties": {
7484+
"type": "string"
7485+
},
7486+
"type": [
7487+
"object",
7488+
"null"
7489+
]
7490+
},
74827491
"threadId": {
74837492
"type": [
74847493
"string",

codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4120,6 +4120,15 @@
41204120
"null"
41214121
]
41224122
},
4123+
"tags": {
4124+
"additionalProperties": {
4125+
"type": "string"
4126+
},
4127+
"type": [
4128+
"object",
4129+
"null"
4130+
]
4131+
},
41234132
"threadId": {
41244133
"type": [
41254134
"string",

codex-rs/app-server-protocol/schema/json/v2/FeedbackUploadParams.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@
2222
"null"
2323
]
2424
},
25+
"tags": {
26+
"additionalProperties": {
27+
"type": "string"
28+
},
29+
"type": [
30+
"object",
31+
"null"
32+
]
33+
},
2534
"threadId": {
2635
"type": [
2736
"string",

codex-rs/app-server-protocol/schema/typescript/v2/FeedbackUploadParams.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
44

5-
export type FeedbackUploadParams = { classification: string, reason?: string | null, threadId?: string | null, includeLogs: boolean, extraLogFiles?: Array<string> | null, };
5+
export type FeedbackUploadParams = { classification: string, reason?: string | null, threadId?: string | null, includeLogs: boolean, extraLogFiles?: Array<string> | null, tags?: { [key in string]?: string } | null, };

codex-rs/app-server-protocol/src/protocol/v2.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2242,6 +2242,8 @@ pub struct FeedbackUploadParams {
22422242
pub include_logs: bool,
22432243
#[ts(optional = nullable)]
22442244
pub extra_log_files: Option<Vec<PathBuf>>,
2245+
#[ts(optional = nullable)]
2246+
pub tags: Option<BTreeMap<String, String>>,
22452247
}
22462248

22472249
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ use codex_features::FEATURES;
243243
use codex_features::Feature;
244244
use codex_features::Stage;
245245
use codex_feedback::CodexFeedback;
246+
use codex_feedback::FeedbackUploadOptions;
246247
use codex_git_utils::git_diff_to_remote;
247248
use codex_git_utils::resolve_root_git_project_for_trust;
248249
use codex_login::AuthManager;
@@ -7726,6 +7727,7 @@ impl CodexMessageProcessor {
77267727
thread_id,
77277728
include_logs,
77287729
extra_log_files,
7730+
tags,
77297731
} = params;
77307732

77317733
let conversation_id = match thread_id.as_deref() {
@@ -7853,14 +7855,15 @@ impl CodexMessageProcessor {
78537855
let session_source = self.thread_manager.session_source();
78547856

78557857
let upload_result = tokio::task::spawn_blocking(move || {
7856-
snapshot.upload_feedback(
7857-
&classification,
7858-
reason.as_deref(),
7858+
snapshot.upload_feedback(FeedbackUploadOptions {
7859+
classification: &classification,
7860+
reason: reason.as_deref(),
7861+
tags: tags.as_ref(),
78597862
include_logs,
7860-
&attachment_paths,
7861-
Some(session_source),
7862-
sqlite_feedback_logs,
7863-
)
7863+
extra_attachment_paths: &attachment_paths,
7864+
session_source: Some(session_source),
7865+
logs_override: sqlite_feedback_logs,
7866+
})
78647867
})
78657868
.await;
78667869

codex-rs/feedback/src/lib.rs

Lines changed: 145 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,16 @@ pub struct FeedbackSnapshot {
338338
pub thread_id: String,
339339
}
340340

341+
pub struct FeedbackUploadOptions<'a> {
342+
pub classification: &'a str,
343+
pub reason: Option<&'a str>,
344+
pub tags: Option<&'a BTreeMap<String, String>>,
345+
pub include_logs: bool,
346+
pub extra_attachment_paths: &'a [PathBuf],
347+
pub session_source: Option<SessionSource>,
348+
pub logs_override: Option<Vec<u8>>,
349+
}
350+
341351
impl FeedbackSnapshot {
342352
pub(crate) fn as_bytes(&self) -> &[u8] {
343353
&self.bytes
@@ -369,16 +379,7 @@ impl FeedbackSnapshot {
369379
}
370380

371381
/// Upload feedback to Sentry with optional attachments.
372-
pub fn upload_feedback(
373-
&self,
374-
classification: &str,
375-
reason: Option<&str>,
376-
include_logs: bool,
377-
extra_attachment_paths: &[PathBuf],
378-
session_source: Option<SessionSource>,
379-
logs_override: Option<Vec<u8>>,
380-
) -> Result<()> {
381-
use std::collections::BTreeMap;
382+
pub fn upload_feedback(&self, options: FeedbackUploadOptions<'_>) -> Result<()> {
382383
use std::str::FromStr;
383384
use std::sync::Arc;
384385

@@ -398,44 +399,22 @@ impl FeedbackSnapshot {
398399
..Default::default()
399400
});
400401

401-
let cli_version = env!("CARGO_PKG_VERSION");
402-
let mut tags = BTreeMap::from([
403-
(String::from("thread_id"), self.thread_id.to_string()),
404-
(String::from("classification"), classification.to_string()),
405-
(String::from("cli_version"), cli_version.to_string()),
406-
]);
407-
if let Some(source) = session_source.as_ref() {
408-
tags.insert(String::from("session_source"), source.to_string());
409-
}
410-
if let Some(r) = reason {
411-
tags.insert(String::from("reason"), r.to_string());
412-
}
413-
414-
let reserved = [
415-
"thread_id",
416-
"classification",
417-
"cli_version",
418-
"session_source",
419-
"reason",
420-
];
421-
for (key, value) in &self.tags {
422-
if reserved.contains(&key.as_str()) {
423-
continue;
424-
}
425-
if let Entry::Vacant(entry) = tags.entry(key.clone()) {
426-
entry.insert(value.clone());
427-
}
428-
}
402+
let tags = self.upload_tags(
403+
options.classification,
404+
options.reason,
405+
options.tags,
406+
options.session_source.as_ref(),
407+
);
429408

430-
let level = match classification {
409+
let level = match options.classification {
431410
"bug" | "bad_result" | "safety_check" => Level::Error,
432411
_ => Level::Info,
433412
};
434413

435414
let mut envelope = Envelope::new();
436415
let title = format!(
437416
"[{}]: Codex session {}",
438-
display_classification(classification),
417+
display_classification(options.classification),
439418
self.thread_id
440419
);
441420

@@ -445,7 +424,7 @@ impl FeedbackSnapshot {
445424
tags,
446425
..Default::default()
447426
};
448-
if let Some(r) = reason {
427+
if let Some(r) = options.reason {
449428
use sentry::protocol::Exception;
450429
use sentry::protocol::Values;
451430

@@ -457,9 +436,11 @@ impl FeedbackSnapshot {
457436
}
458437
envelope.add_item(EnvelopeItem::Event(event));
459438

460-
for attachment in
461-
self.feedback_attachments(include_logs, extra_attachment_paths, logs_override)
462-
{
439+
for attachment in self.feedback_attachments(
440+
options.include_logs,
441+
options.extra_attachment_paths,
442+
options.logs_override,
443+
) {
463444
envelope.add_item(EnvelopeItem::Attachment(attachment));
464445
}
465446

@@ -468,6 +449,55 @@ impl FeedbackSnapshot {
468449
Ok(())
469450
}
470451

452+
fn upload_tags(
453+
&self,
454+
classification: &str,
455+
reason: Option<&str>,
456+
client_tags: Option<&BTreeMap<String, String>>,
457+
session_source: Option<&SessionSource>,
458+
) -> BTreeMap<String, String> {
459+
let cli_version = env!("CARGO_PKG_VERSION");
460+
let mut tags = BTreeMap::from([
461+
(String::from("thread_id"), self.thread_id.to_string()),
462+
(String::from("classification"), classification.to_string()),
463+
(String::from("cli_version"), cli_version.to_string()),
464+
]);
465+
if let Some(source) = session_source {
466+
tags.insert(String::from("session_source"), source.to_string());
467+
}
468+
if let Some(r) = reason {
469+
tags.insert(String::from("reason"), r.to_string());
470+
}
471+
472+
let reserved = [
473+
"thread_id",
474+
"classification",
475+
"cli_version",
476+
"session_source",
477+
"reason",
478+
];
479+
if let Some(client_tags) = client_tags {
480+
for (key, value) in client_tags {
481+
if reserved.contains(&key.as_str()) {
482+
continue;
483+
}
484+
if let Entry::Vacant(entry) = tags.entry(key.clone()) {
485+
entry.insert(value.clone());
486+
}
487+
}
488+
}
489+
for (key, value) in &self.tags {
490+
if reserved.contains(&key.as_str()) {
491+
continue;
492+
}
493+
if let Entry::Vacant(entry) = tags.entry(key.clone()) {
494+
entry.insert(value.clone());
495+
}
496+
}
497+
498+
tags
499+
}
500+
471501
fn feedback_attachments(
472502
&self,
473503
include_logs: bool,
@@ -697,4 +727,75 @@ mod tests {
697727
assert_eq!(attachments_without_diagnostics[0].buffer, vec![1]);
698728
fs::remove_file(extra_path).expect("extra attachment should be removed");
699729
}
730+
731+
#[test]
732+
fn upload_tags_include_client_tags_and_preserve_reserved_fields() {
733+
let mut tags = BTreeMap::new();
734+
tags.insert("thread_id".to_string(), "wrong-thread".to_string());
735+
tags.insert("turn_id".to_string(), "wrong-turn".to_string());
736+
tags.insert(
737+
"classification".to_string(),
738+
"wrong-classification".to_string(),
739+
);
740+
tags.insert("cli_version".to_string(), "wrong-version".to_string());
741+
tags.insert("session_source".to_string(), "wrong-source".to_string());
742+
tags.insert("reason".to_string(), "wrong-reason".to_string());
743+
tags.insert("model".to_string(), "gpt-5".to_string());
744+
let snapshot = FeedbackSnapshot {
745+
bytes: Vec::new(),
746+
tags,
747+
feedback_diagnostics: FeedbackDiagnostics::default(),
748+
thread_id: "thread-123".to_string(),
749+
};
750+
let mut client_tags = BTreeMap::new();
751+
client_tags.insert("thread_id".to_string(), "wrong-client-thread".to_string());
752+
client_tags.insert("turn_id".to_string(), "turn-456".to_string());
753+
client_tags.insert(
754+
"classification".to_string(),
755+
"wrong-client-classification".to_string(),
756+
);
757+
client_tags.insert(
758+
"cli_version".to_string(),
759+
"wrong-client-version".to_string(),
760+
);
761+
client_tags.insert(
762+
"session_source".to_string(),
763+
"wrong-client-source".to_string(),
764+
);
765+
client_tags.insert("reason".to_string(), "wrong-client-reason".to_string());
766+
client_tags.insert("client_tag".to_string(), "from-client".to_string());
767+
768+
let upload_tags = snapshot.upload_tags(
769+
"bug",
770+
Some("actual reason"),
771+
Some(&client_tags),
772+
Some(&SessionSource::Cli),
773+
);
774+
775+
assert_eq!(
776+
upload_tags.get("thread_id").map(String::as_str),
777+
Some("thread-123")
778+
);
779+
assert_eq!(
780+
upload_tags.get("turn_id").map(String::as_str),
781+
Some("turn-456")
782+
);
783+
assert_eq!(
784+
upload_tags.get("classification").map(String::as_str),
785+
Some("bug")
786+
);
787+
assert_eq!(
788+
upload_tags.get("session_source").map(String::as_str),
789+
Some("cli")
790+
);
791+
assert_eq!(
792+
upload_tags.get("reason").map(String::as_str),
793+
Some("actual reason")
794+
);
795+
assert_eq!(
796+
upload_tags.get("client_tag").map(String::as_str),
797+
Some("from-client")
798+
);
799+
assert_eq!(upload_tags.get("model").map(String::as_str), Some("gpt-5"));
800+
}
700801
}

0 commit comments

Comments
 (0)