@@ -3051,12 +3051,22 @@ export function REPL({
30513051 // are O(n) per render, so drop everything before the previous
30523052 // boundary to keep n bounded across multi-day sessions.
30533053 if ( isFullscreenEnvEnabled ( ) ) {
3054- setMessages ( old => [
3055- ... getMessagesAfterCompactBoundary ( old , {
3054+ setMessages ( old => {
3055+ const postBoundary = getMessagesAfterCompactBoundary ( old , {
30563056 includeSnipped : true ,
3057- } ) ,
3058- newMessage ,
3059- ] ) ;
3057+ } )
3058+ // Hard cap: keep at most 500 messages in fullscreen scrollback
3059+ // to prevent unbounded memory growth in multi-day sessions.
3060+ // normalizeMessages/applyGrouping are O(n), and Ink fiber
3061+ // trees cost ~250KB RSS per message. Without this cap,
3062+ // scrollback after several compactions can reach thousands
3063+ // of messages (observed: 13k+, 1GB+ heap).
3064+ const MAX_FULLSCREEN_SCROLLBACK = 500
3065+ const kept = postBoundary . length > MAX_FULLSCREEN_SCROLLBACK
3066+ ? postBoundary . slice ( - MAX_FULLSCREEN_SCROLLBACK )
3067+ : postBoundary
3068+ return [ ...kept , newMessage ]
3069+ } ) ;
30603070 } else {
30613071 setMessages ( ( ) => [ newMessage ] ) ;
30623072 }
@@ -3082,17 +3092,23 @@ export function REPL({
30823092 // history). Replacing those leaves the AgentTool UI stuck at
30833093 // "Initializing…" because it renders the full progress trail.
30843094 setMessages ( oldMessages => {
3085- const last = oldMessages . at ( - 1 ) ;
3086- const lastData = last ?. data as Record < string , unknown > | undefined ;
30873095 const newData = newMessage . data as Record < string , unknown > ;
3088- if (
3089- last ?. type === 'progress' &&
3090- last . parentToolUseID === newMessage . parentToolUseID &&
3091- lastData ?. type === newData . type
3092- ) {
3093- const copy = oldMessages . slice ( ) ;
3094- copy [ copy . length - 1 ] = newMessage ;
3095- return copy ;
3096+ // Scan backwards to find the last ephemeral progress with matching
3097+ // parentToolUseID and type. Previously only checked the last message,
3098+ // so interleaved non-ephemeral messages caused duplicate progress
3099+ // entries to accumulate (observed 13k+ entries in sleep-heavy sessions).
3100+ for ( let i = oldMessages . length - 1 ; i >= 0 ; i -- ) {
3101+ const m = oldMessages [ i ] !
3102+ if ( m . type !== 'progress' ) break
3103+ const mData = m . data as Record < string , unknown > | undefined
3104+ if (
3105+ m . parentToolUseID === newMessage . parentToolUseID &&
3106+ mData ?. type === newData . type
3107+ ) {
3108+ const copy = oldMessages . slice ( ) ;
3109+ copy [ i ] = newMessage ;
3110+ return copy ;
3111+ }
30963112 }
30973113 return [ ...oldMessages , newMessage ] ;
30983114 } ) ;
0 commit comments