Files
codex/codex-rs/core/src/context/realtime_delegation.rs
T
Peter Bakkum ecb8013dfa Reconnect WebRTC Realtime sideband transports (#39257)
## What changed

- Reconnect frameless bidirectional WebRTC sideband sockets after unexpected transport loss with capped exponential backoff.
- Preserve pending text or handoff output and bounded transcript state across reconnects.
- Treat `404 Not Found` and `410 Gone` handshake responses as terminal session completion, and preserve HTTP status information from failed WebSocket handshakes.
- Bound Realtime delegation input and transcript fields while retaining the most relevant portion of each.

## Testing

- Add unit coverage for reconnect backoff, terminal status handling, transport error classification, transcript continuity and truncation, and pending outbound messages.
- Add an end-to-end WebRTC conversation test covering an unclean disconnect, reconnect, transcript reconciliation, outbound progress during sustained inbound traffic, and terminal session completion.

GitOrigin-RevId: 544fd7e542c3434d0bb42cfbcc7143604419a037
2026-08-18 18:41:40 +00:00

101 lines
2.8 KiB
Rust

use super::ContextualUserFragment;
const MAX_REALTIME_DELEGATION_FIELD_BYTES: usize = 4 * 1024;
const TRUNCATION_MARKER: &str = "…";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RealtimeDelegationSource {
Handoff,
TranscriptTailFlush,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RealtimeDelegation<'a> {
input: &'a str,
transcript_delta: Option<&'a str>,
source: RealtimeDelegationSource,
}
impl<'a> RealtimeDelegation<'a> {
pub(crate) fn new(
input: &'a str,
transcript_delta: Option<&'a str>,
source: RealtimeDelegationSource,
) -> Self {
Self {
input,
transcript_delta,
source,
}
}
}
impl ContextualUserFragment for RealtimeDelegation<'_> {
fn role(&self) -> &'static str {
"user"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
("<realtime_delegation>", "</realtime_delegation>")
}
fn body(&self) -> String {
let input = escape_xml_text_bounded(self.input, Retain::Start);
let source = match self.source {
RealtimeDelegationSource::Handoff => "",
RealtimeDelegationSource::TranscriptTailFlush => {
" <source>transcript_tail_flush</source>\n"
}
};
if let Some(transcript_delta) = self.transcript_delta.filter(|text| !text.is_empty()) {
let transcript_delta = escape_xml_text_bounded(transcript_delta, Retain::End);
return format!(
"\n{source} <input>{input}</input>\n <transcript_delta>{transcript_delta}</transcript_delta>\n"
);
}
format!("\n{source} <input>{input}</input>\n")
}
}
#[derive(Clone, Copy)]
enum Retain {
Start,
End,
}
fn escape_xml_text_bounded(input: &str, retain: Retain) -> String {
let escaped = escape_xml_text(input);
if escaped.len() <= MAX_REALTIME_DELEGATION_FIELD_BYTES {
return escaped;
}
let retained_bytes = MAX_REALTIME_DELEGATION_FIELD_BYTES - TRUNCATION_MARKER.len();
match retain {
Retain::Start => {
let mut end = retained_bytes;
while !escaped.is_char_boundary(end) {
end -= 1;
}
format!("{}{TRUNCATION_MARKER}", &escaped[..end])
}
Retain::End => {
let mut start = escaped.len() - retained_bytes;
while !escaped.is_char_boundary(start) {
start += 1;
}
format!("{TRUNCATION_MARKER}{}", &escaped[start..])
}
}
}
fn escape_xml_text(input: &str) -> String {
input
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}