diff --git a/crates/protocol/src/metadata.rs b/crates/protocol/src/metadata.rs index d5989e839..a9e11ed49 100644 --- a/crates/protocol/src/metadata.rs +++ b/crates/protocol/src/metadata.rs @@ -331,7 +331,10 @@ fn resolve_path(headers: &http::HeaderMap, path: &str) -> Option { } match current { - serde_json::Value::String(s) => Some(s), + serde_json::Value::String(s) => { + let value = s.trim(); + (!value.is_empty()).then(|| value.to_string()) + } serde_json::Value::Null => None, leaf => Some(leaf.to_string()), } @@ -540,6 +543,27 @@ mod tests { assert_eq!(sy_header(&headers, "x-not-a-field"), None); } + // Blank nested metadata must not mask a valid lower-priority session header. + #[test] + fn nested_metadata_strings_match_flat_header_normalization() { + let body = serde_json::json!({ "session_id": " codex-session " }).to_string(); + let headers = slice_to_header_map(&[(CODEX_TURN_METADATA_HEADER, body.as_str())]); + assert_eq!( + sy_header(&headers, SWITCHYARD_SESSION_ID_HEADER).as_deref(), + Some("codex-session") + ); + + let blank_body = serde_json::json!({ "session_id": " " }).to_string(); + let headers = slice_to_header_map(&[ + (CODEX_TURN_METADATA_HEADER, blank_body.as_str()), + (SESSION_ID_HEADER, "fallback-session"), + ]); + assert_eq!( + sy_header(&headers, SWITCHYARD_SESSION_ID_HEADER).as_deref(), + Some("fallback-session") + ); + } + #[test] fn subagent_routing_honors_explicit_signals_and_delegated_work_kinds() { // Explicit `false` wins over presence-based inference even when no