fix(chat): borner le scrollback de ChatBridge (#34)
Le scrollback de ChatBridge croissait sans limite, faisant enfler la mémoire sur les conversations longues. Il sert de buffer de replay transport (reattach), pas d'historique durable. Introduit un double cap : MAX_CHAT_SCROLLBACK_CHUNKS=2000 et MAX_CHAT_SCROLLBACK_BYTES=512 KiB. Un helper trim_scrollback, appelé après chaque send_output, drop des chunks entiers par la tête tout en conservant l'ordre et les chunks les plus récents. Doc de chat.rs corrigée pour refléter cette nature de buffer borné. Aucun changement de ReplyChunk / reattach_agent_chat / DTO. Tests : suite chat_bridge 19/19. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -33,6 +33,11 @@ use domain::ports::ReplyEvent;
|
||||
|
||||
use crate::dto::ReplyChunk;
|
||||
|
||||
/// Maximum number of recent chat chunks retained for transport reattach replay.
|
||||
pub const MAX_CHAT_SCROLLBACK_CHUNKS: usize = 2_000;
|
||||
/// Maximum estimated bytes retained for transport reattach replay.
|
||||
pub const MAX_CHAT_SCROLLBACK_BYTES: usize = 512 * 1024;
|
||||
|
||||
/// Per-session transport state: the current attach generation, its output
|
||||
/// channel, and the conversation scrollback recorded so far.
|
||||
struct ChatEntry {
|
||||
@ -42,9 +47,9 @@ struct ChatEntry {
|
||||
/// when the session has been recorded (scrollback kept) but no view is
|
||||
/// currently attached — the pump then only appends to the scrollback.
|
||||
channel: Option<Channel<ReplyChunk>>,
|
||||
/// Every chunk routed for this session, in order — the conversation
|
||||
/// scrollback replayed on re-attach. Bounded only by the conversation length
|
||||
/// (a turn count), like the PTY ring buffer is bounded by its byte capacity.
|
||||
/// Recent chunks retained for reattach, bounded by chunk count and estimated
|
||||
/// byte size; older chunks are dropped from the head. This is a transport
|
||||
/// replay buffer, not durable conversation history.
|
||||
scrollback: Vec<ReplyChunk>,
|
||||
}
|
||||
|
||||
@ -159,6 +164,7 @@ impl ChatBridge {
|
||||
return false;
|
||||
};
|
||||
entry.scrollback.push(chunk.clone());
|
||||
trim_scrollback(entry);
|
||||
match &entry.channel {
|
||||
Some(channel) => channel.send(chunk).is_ok(),
|
||||
None => false,
|
||||
@ -172,6 +178,27 @@ impl ChatBridge {
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_scrollback(entry: &mut ChatEntry) {
|
||||
while !entry.scrollback.is_empty()
|
||||
&& (entry.scrollback.len() > MAX_CHAT_SCROLLBACK_CHUNKS
|
||||
|| scrollback_bytes(&entry.scrollback) > MAX_CHAT_SCROLLBACK_BYTES)
|
||||
{
|
||||
entry.scrollback.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn scrollback_bytes(chunks: &[ReplyChunk]) -> usize {
|
||||
chunks.iter().map(reply_chunk_bytes).sum()
|
||||
}
|
||||
|
||||
fn reply_chunk_bytes(chunk: &ReplyChunk) -> usize {
|
||||
match chunk {
|
||||
ReplyChunk::TextDelta { text } => text.len(),
|
||||
ReplyChunk::ToolActivity { label } => label.len(),
|
||||
ReplyChunk::Final { content } => content.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a domain [`ReplyEvent`] to its wire [`ReplyChunk`]. Pure translation, no
|
||||
/// I/O — the single point where the typed turn event becomes a serialisable chunk
|
||||
/// (kept here so the pump and any test share one mapping).
|
||||
|
||||
Reference in New Issue
Block a user