Files
codex/codex-rs/tui/src/app/thread_event_buffer.rs
T
Tamir Duberstein 9c099e94a2 Bound TUI thread replay buffers by delta size (#39081)
## Why

The per-thread replay buffer limited its event count, but streamed agent-message
deltas could still retain an unbounded amount of text while a thread was
inactive.

## What changed

- Coalesce adjacent deltas for the same thread, turn, and item into chunks of up
  to 4 KiB.
- Limit buffered agent-message deltas to 256 KiB, discard a single delta larger
  than that limit, and evict the oldest events when either buffer limit is
  exceeded.
- Keep pending input and approval state in sync when eviction removes the
  corresponding request, including clearing side-thread status indicators.

## Testing

Added coverage for delta coalescing, byte-based eviction, oversized deltas,
replaying only retained text, and side-thread status updates after eviction.

GitOrigin-RevId: 88d85353a124467d467822d2fd9854312b68d547
2026-08-17 21:03:06 +00:00

82 lines
3.3 KiB
Rust

//! Bounded replay-buffer policy for per-thread app-server events.
use super::ServerNotification;
use super::ThreadBufferedEvent;
use super::ThreadEventStore;
use std::borrow::Cow;
// Keep merged text finite so continued streaming still reaches bounded replay eviction.
const MAX_COALESCED_AGENT_MESSAGE_DELTA_BYTES: usize = 4 * 1024;
const MAX_BUFFERED_AGENT_MESSAGE_DELTA_BYTES: usize = 256 * 1024;
impl ThreadEventStore {
pub(super) fn push_replay_notification(&mut self, notification: Cow<'_, ServerNotification>) {
if let ServerNotification::AgentMessageDelta(delta) = notification.as_ref()
&& delta.delta.len() > MAX_BUFFERED_AGENT_MESSAGE_DELTA_BYTES
{
return;
}
if let ServerNotification::AgentMessageDelta(delta) = notification.as_ref()
&& let Some(ThreadBufferedEvent::Notification(previous)) = self.buffer.back_mut()
&& let ServerNotification::AgentMessageDelta(previous) = previous.as_mut()
&& previous.thread_id == delta.thread_id
&& previous.turn_id == delta.turn_id
&& previous.item_id == delta.item_id
&& previous.delta.len().saturating_add(delta.delta.len())
<= MAX_COALESCED_AGENT_MESSAGE_DELTA_BYTES
{
previous.delta.push_str(&delta.delta);
self.buffered_agent_message_delta_bytes = self
.buffered_agent_message_delta_bytes
.saturating_add(delta.delta.len());
self.evict_overflowing_events();
return;
}
self.push_buffered_event(ThreadBufferedEvent::Notification(Box::new(
notification.into_owned(),
)));
}
pub(super) fn push_buffered_event(&mut self, event: ThreadBufferedEvent) {
if let ThreadBufferedEvent::Notification(notification) = &event
&& let ServerNotification::AgentMessageDelta(delta) = notification.as_ref()
{
self.buffered_agent_message_delta_bytes = self
.buffered_agent_message_delta_bytes
.saturating_add(delta.delta.len());
}
self.buffer.push_back(event);
self.evict_overflowing_events();
}
fn evict_overflowing_events(&mut self) {
while self.buffer.len() > self.capacity
|| self.buffered_agent_message_delta_bytes > MAX_BUFFERED_AGENT_MESSAGE_DELTA_BYTES
{
let Some(removed) = self.buffer.pop_front() else {
break;
};
match removed {
ThreadBufferedEvent::Notification(notification) => {
if let ServerNotification::AgentMessageDelta(delta) = notification.as_ref() {
self.buffered_agent_message_delta_bytes = self
.buffered_agent_message_delta_bytes
.saturating_sub(delta.delta.len());
}
}
ThreadBufferedEvent::Request(request) => self
.pending_interactive_replay
.note_evicted_server_request(request.as_ref()),
ThreadBufferedEvent::HistoryEntryResponse(_)
| ThreadBufferedEvent::FeedbackSubmission(_) => {}
}
}
}
}
#[cfg(test)]
#[path = "thread_event_buffer_tests.rs"]
mod tests;