From f3531c92c742aa120e2a7bd25649db9e1d21bd28 Mon Sep 17 00:00:00 2001 From: Javier Soto Date: Fri, 20 Feb 2026 14:36:30 -0800 Subject: [PATCH] [WIP] app-server: fix schema export behavior with typename clashes --- codex-rs/app-server-protocol/src/export.rs | 80 ++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index ec51dd1585..9c6948ae43 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -936,6 +936,14 @@ fn insert_into_namespace( .or_insert_with(|| Value::Object(Map::new())); match entry { Value::Object(map) => { + if let Some(existing) = map.get_mut(&name) { + if existing != &schema { + if let Some(merged) = merge_compatible_schema_definitions(existing, &schema) { + *existing = merged; + } + } + return Ok(()); + } map.insert(name, schema); Ok(()) } @@ -1048,6 +1056,78 @@ fn collect_namespaced_types(schemas: &[GeneratedSchema]) -> HashMap Option { + if let (Some(existing_variants), Some(incoming_variants)) = ( + enum_literals_from_schema(existing), + enum_literals_from_schema(incoming), + ) { + let mut merged = existing_variants.clone(); + for value in incoming_variants { + if !merged.contains(&value) { + merged.push(value); + } + } + if let Some(schema) = schema_with_replaced_enum_literals(existing, &merged) { + return Some(schema); + } + if let Some(schema) = schema_with_replaced_enum_literals(incoming, &merged) { + return Some(schema); + } + } + None +} + +fn enum_literals_from_schema(schema: &Value) -> Option> { + let object = schema.as_object()?; + if object.get("type") == Some(&Value::String("string".to_string())) { + let enum_values = object.get("enum")?.as_array()?; + return enum_values + .iter() + .map(|value| value.as_str().map(str::to_string)) + .collect(); + } + + let one_of = object.get("oneOf")?.as_array()?; + let mut values = Vec::new(); + for variant in one_of { + let variant_object = variant.as_object()?; + if variant_object.get("type") != Some(&Value::String("string".to_string())) { + return None; + } + let enum_values = variant_object.get("enum")?.as_array()?; + if enum_values.len() != 1 { + return None; + } + values.push(enum_values[0].as_str()?.to_string()); + } + Some(values) +} + +fn schema_with_replaced_enum_literals(schema: &Value, values: &[String]) -> Option { + let mut updated = schema.clone(); + let object = updated.as_object_mut()?; + if object.get("type") == Some(&Value::String("string".to_string())) { + object.insert( + "enum".to_string(), + Value::Array(values.iter().cloned().map(Value::String).collect()), + ); + return Some(updated); + } + + let one_of = object.get_mut("oneOf")?.as_array_mut()?; + if one_of.len() != values.len() { + return None; + } + for (variant, value) in one_of.iter_mut().zip(values) { + let variant_object = variant.as_object_mut()?; + variant_object.insert( + "enum".to_string(), + Value::Array(vec![Value::String(value.clone())]), + ); + } + Some(updated) +} + fn namespace_for_definition<'a>( name: &str, types: &'a HashMap,