@@ -163,3 +163,77 @@ export function isSnipRuntimeEnabled(): boolean {
163163export function shouldNudgeForSnips ( messages : Message [ ] ) : boolean {
164164 return messages . length >= SNIP_NUDGE_THRESHOLD
165165}
166+
167+ /**
168+ * Maximum total character length of message content before proactive
169+ * truncation kicks in. ~150 MB of string data corresponds to roughly
170+ * 1.5x the default 200k-token context window at 4 chars/token — well
171+ * beyond what any model can actually use in a single request.
172+ */
173+ const PROACTIVE_TRUNCATE_CHARS = 150_000_000
174+
175+ /**
176+ * Minimum number of messages to keep when falling back to tail-only
177+ * retention (i.e. when no compact_boundary exists in the array).
178+ */
179+ const PROACTIVE_TRUNCATE_MIN_TAIL = 50
180+
181+ /**
182+ * Proactively truncate old messages when the in-memory store grows too
183+ * large. Unlike `snipCompactIfNeeded` (which waits for a snip_boundary
184+ * from the API), this runs client-side after every push — ensuring
185+ * unbounded growth cannot happen even when the API never returns a
186+ * compact_boundary (e.g. third-party compat layers).
187+ *
188+ * Strategy:
189+ * 1. If a `compact_boundary` exists, keep it and everything after it.
190+ * 2. Otherwise, keep only the last `PROACTIVE_TRUNCATE_MIN_TAIL` messages.
191+ *
192+ * Returns the same array reference when no truncation is needed.
193+ */
194+ export function proactiveTruncate ( messages : Message [ ] ) : Message [ ] {
195+ if ( messages . length < PROACTIVE_TRUNCATE_MIN_TAIL ) return messages
196+
197+ let totalChars = 0
198+ for ( const msg of messages ) {
199+ const content = msg . message ?. content
200+ if ( typeof content === 'string' ) {
201+ totalChars += content . length
202+ } else if ( Array . isArray ( content ) ) {
203+ for ( const block of content ) {
204+ if ( typeof block === 'string' ) {
205+ totalChars += ( block as string ) . length
206+ } else if ( block && typeof block === 'object' ) {
207+ const obj = block as unknown as Record < string , unknown >
208+ const text = obj . text ?? obj . content
209+ if ( typeof text === 'string' ) {
210+ totalChars += text . length
211+ }
212+ }
213+ }
214+ }
215+ }
216+
217+ if ( totalChars < PROACTIVE_TRUNCATE_CHARS ) return messages
218+
219+ // Find last compact_boundary — the standard anchor point
220+ let boundaryIdx = - 1
221+ for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
222+ const msg = messages [ i ] !
223+ if (
224+ msg . type === 'system' &&
225+ ( msg as Record < string , unknown > ) . subtype === 'compact_boundary'
226+ ) {
227+ boundaryIdx = i
228+ break
229+ }
230+ }
231+
232+ const keepFrom =
233+ boundaryIdx >= 0
234+ ? boundaryIdx
235+ : Math . max ( 0 , messages . length - PROACTIVE_TRUNCATE_MIN_TAIL )
236+ if ( keepFrom === 0 ) return messages
237+
238+ return messages . slice ( keepFrom )
239+ }
0 commit comments