11import type { AutoCompactState , FallbackState , RetryState , TruncateState } from "./types"
2+ import type { ExperimentalConfig } from "../../config"
23import { FALLBACK_CONFIG , RETRY_CONFIG , TRUNCATE_CONFIG } from "./types"
3- import { findLargestToolResult , truncateToolResult } from "./storage"
4+ import { findLargestToolResult , truncateToolResult , truncateUntilTargetTokens } from "./storage"
5+ import { findEmptyMessages , injectTextPart } from "../session-recovery/storage"
6+ import { log } from "../../shared/logger"
47
58type Client = {
69 session : {
@@ -151,24 +154,151 @@ function clearSessionState(autoCompactState: AutoCompactState, sessionID: string
151154 autoCompactState . retryStateBySession . delete ( sessionID )
152155 autoCompactState . fallbackStateBySession . delete ( sessionID )
153156 autoCompactState . truncateStateBySession . delete ( sessionID )
157+ autoCompactState . emptyContentAttemptBySession . delete ( sessionID )
154158 autoCompactState . compactionInProgress . delete ( sessionID )
155159}
156160
161+ function getOrCreateEmptyContentAttempt (
162+ autoCompactState : AutoCompactState ,
163+ sessionID : string
164+ ) : number {
165+ return autoCompactState . emptyContentAttemptBySession . get ( sessionID ) ?? 0
166+ }
167+
168+ async function fixEmptyMessages (
169+ sessionID : string ,
170+ autoCompactState : AutoCompactState ,
171+ client : Client
172+ ) : Promise < boolean > {
173+ const attempt = getOrCreateEmptyContentAttempt ( autoCompactState , sessionID )
174+ autoCompactState . emptyContentAttemptBySession . set ( sessionID , attempt + 1 )
175+
176+ const emptyMessageIds = findEmptyMessages ( sessionID )
177+ if ( emptyMessageIds . length === 0 ) {
178+ await client . tui
179+ . showToast ( {
180+ body : {
181+ title : "Empty Content Error" ,
182+ message : "No empty messages found in storage. Cannot auto-recover." ,
183+ variant : "error" ,
184+ duration : 5000 ,
185+ } ,
186+ } )
187+ . catch ( ( ) => { } )
188+ return false
189+ }
190+
191+ let fixed = false
192+ for ( const messageID of emptyMessageIds ) {
193+ const success = injectTextPart ( sessionID , messageID , "[user interrupted]" )
194+ if ( success ) fixed = true
195+ }
196+
197+ if ( fixed ) {
198+ await client . tui
199+ . showToast ( {
200+ body : {
201+ title : "Session Recovery" ,
202+ message : `Fixed ${ emptyMessageIds . length } empty messages. Retrying...` ,
203+ variant : "warning" ,
204+ duration : 3000 ,
205+ } ,
206+ } )
207+ . catch ( ( ) => { } )
208+ }
209+
210+ return fixed
211+ }
212+
157213export async function executeCompact (
158214 sessionID : string ,
159215 msg : Record < string , unknown > ,
160216 autoCompactState : AutoCompactState ,
161217 // eslint-disable-next-line @typescript-eslint/no-explicit-any
162218 client : any ,
163- directory : string
219+ directory : string ,
220+ experimental ?: ExperimentalConfig
164221) : Promise < void > {
165222 if ( autoCompactState . compactionInProgress . has ( sessionID ) ) {
166223 return
167224 }
168225 autoCompactState . compactionInProgress . add ( sessionID )
169226
227+ const errorData = autoCompactState . errorDataBySession . get ( sessionID )
170228 const truncateState = getOrCreateTruncateState ( autoCompactState , sessionID )
171229
230+ if (
231+ experimental ?. aggressive_truncation &&
232+ errorData ?. currentTokens &&
233+ errorData ?. maxTokens &&
234+ errorData . currentTokens > errorData . maxTokens &&
235+ truncateState . truncateAttempt < TRUNCATE_CONFIG . maxTruncateAttempts
236+ ) {
237+ log ( "[auto-compact] aggressive truncation triggered (experimental)" , {
238+ currentTokens : errorData . currentTokens ,
239+ maxTokens : errorData . maxTokens ,
240+ targetRatio : TRUNCATE_CONFIG . targetTokenRatio ,
241+ } )
242+
243+ const aggressiveResult = truncateUntilTargetTokens (
244+ sessionID ,
245+ errorData . currentTokens ,
246+ errorData . maxTokens ,
247+ TRUNCATE_CONFIG . targetTokenRatio ,
248+ TRUNCATE_CONFIG . charsPerToken
249+ )
250+
251+ if ( aggressiveResult . truncatedCount > 0 ) {
252+ truncateState . truncateAttempt += aggressiveResult . truncatedCount
253+
254+ const toolNames = aggressiveResult . truncatedTools . map ( ( t ) => t . toolName ) . join ( ", " )
255+ const statusMsg = aggressiveResult . sufficient
256+ ? `Truncated ${ aggressiveResult . truncatedCount } outputs (${ formatBytes ( aggressiveResult . totalBytesRemoved ) } )`
257+ : `Truncated ${ aggressiveResult . truncatedCount } outputs (${ formatBytes ( aggressiveResult . totalBytesRemoved ) } ) but need ${ formatBytes ( aggressiveResult . targetBytesToRemove ) } . Falling back to summarize/revert...`
258+
259+ await ( client as Client ) . tui
260+ . showToast ( {
261+ body : {
262+ title : aggressiveResult . sufficient ? "Aggressive Truncation" : "Partial Truncation" ,
263+ message : `${ statusMsg } : ${ toolNames } ` ,
264+ variant : "warning" ,
265+ duration : 4000 ,
266+ } ,
267+ } )
268+ . catch ( ( ) => { } )
269+
270+ log ( "[auto-compact] aggressive truncation completed" , aggressiveResult )
271+
272+ if ( aggressiveResult . sufficient ) {
273+ autoCompactState . compactionInProgress . delete ( sessionID )
274+
275+ setTimeout ( async ( ) => {
276+ try {
277+ await ( client as Client ) . session . prompt_async ( {
278+ path : { sessionID } ,
279+ body : { parts : [ { type : "text" , text : "Continue" } ] } ,
280+ query : { directory } ,
281+ } )
282+ } catch { }
283+ } , 500 )
284+ return
285+ }
286+ } else {
287+ await ( client as Client ) . tui
288+ . showToast ( {
289+ body : {
290+ title : "Truncation Skipped" ,
291+ message : "No tool outputs found to truncate." ,
292+ variant : "warning" ,
293+ duration : 3000 ,
294+ } ,
295+ } )
296+ . catch ( ( ) => { } )
297+ }
298+ }
299+
300+ let skipSummarize = false
301+
172302 if ( truncateState . truncateAttempt < TRUNCATE_CONFIG . maxTruncateAttempts ) {
173303 const largest = findLargestToolResult ( sessionID )
174304
@@ -203,12 +333,68 @@ export async function executeCompact(
203333 } , 500 )
204334 return
205335 }
336+ } else if ( errorData ?. currentTokens && errorData ?. maxTokens && errorData . currentTokens > errorData . maxTokens ) {
337+ skipSummarize = true
338+ await ( client as Client ) . tui
339+ . showToast ( {
340+ body : {
341+ title : "Summarize Skipped" ,
342+ message : `Over token limit (${ errorData . currentTokens } /${ errorData . maxTokens } ) with nothing to truncate. Going to revert...` ,
343+ variant : "warning" ,
344+ duration : 3000 ,
345+ } ,
346+ } )
347+ . catch ( ( ) => { } )
348+ } else if ( ! errorData ?. currentTokens ) {
349+ await ( client as Client ) . tui
350+ . showToast ( {
351+ body : {
352+ title : "Truncation Skipped" ,
353+ message : "No large tool outputs found." ,
354+ variant : "warning" ,
355+ duration : 3000 ,
356+ } ,
357+ } )
358+ . catch ( ( ) => { } )
206359 }
207360 }
208361
209362 const retryState = getOrCreateRetryState ( autoCompactState , sessionID )
210363
211- if ( retryState . attempt < RETRY_CONFIG . maxAttempts ) {
364+ if ( experimental ?. empty_message_recovery && errorData ?. errorType ?. includes ( "non-empty content" ) ) {
365+ const attempt = getOrCreateEmptyContentAttempt ( autoCompactState , sessionID )
366+ if ( attempt < 3 ) {
367+ const fixed = await fixEmptyMessages ( sessionID , autoCompactState , client as Client )
368+ if ( fixed ) {
369+ autoCompactState . compactionInProgress . delete ( sessionID )
370+ setTimeout ( ( ) => {
371+ executeCompact ( sessionID , msg , autoCompactState , client , directory , experimental )
372+ } , 500 )
373+ return
374+ }
375+ } else {
376+ await ( client as Client ) . tui
377+ . showToast ( {
378+ body : {
379+ title : "Recovery Failed" ,
380+ message : "Max recovery attempts (3) reached for empty content error. Please start a new session." ,
381+ variant : "error" ,
382+ duration : 10000 ,
383+ } ,
384+ } )
385+ . catch ( ( ) => { } )
386+ autoCompactState . compactionInProgress . delete ( sessionID )
387+ return
388+ }
389+ }
390+
391+ if ( Date . now ( ) - retryState . lastAttemptTime > 300000 ) {
392+ retryState . attempt = 0
393+ autoCompactState . fallbackStateBySession . delete ( sessionID )
394+ autoCompactState . truncateStateBySession . delete ( sessionID )
395+ }
396+
397+ if ( ! skipSummarize && retryState . attempt < RETRY_CONFIG . maxAttempts ) {
212398 retryState . attempt ++
213399 retryState . lastAttemptTime = Date . now ( )
214400
@@ -234,7 +420,7 @@ export async function executeCompact(
234420 query : { directory } ,
235421 } )
236422
237- clearSessionState ( autoCompactState , sessionID )
423+ autoCompactState . compactionInProgress . delete ( sessionID )
238424
239425 setTimeout ( async ( ) => {
240426 try {
@@ -253,10 +439,21 @@ export async function executeCompact(
253439 const cappedDelay = Math . min ( delay , RETRY_CONFIG . maxDelayMs )
254440
255441 setTimeout ( ( ) => {
256- executeCompact ( sessionID , msg , autoCompactState , client , directory )
442+ executeCompact ( sessionID , msg , autoCompactState , client , directory , experimental )
257443 } , cappedDelay )
258444 return
259445 }
446+ } else {
447+ await ( client as Client ) . tui
448+ . showToast ( {
449+ body : {
450+ title : "Summarize Skipped" ,
451+ message : "Missing providerID or modelID. Skipping to revert..." ,
452+ variant : "warning" ,
453+ duration : 3000 ,
454+ } ,
455+ } )
456+ . catch ( ( ) => { } )
260457 }
261458 }
262459
@@ -301,10 +498,21 @@ export async function executeCompact(
301498 autoCompactState . compactionInProgress . delete ( sessionID )
302499
303500 setTimeout ( ( ) => {
304- executeCompact ( sessionID , msg , autoCompactState , client , directory )
501+ executeCompact ( sessionID , msg , autoCompactState , client , directory , experimental )
305502 } , 1000 )
306503 return
307504 } catch { }
505+ } else {
506+ await ( client as Client ) . tui
507+ . showToast ( {
508+ body : {
509+ title : "Revert Skipped" ,
510+ message : "Could not find last message pair to revert." ,
511+ variant : "warning" ,
512+ duration : 3000 ,
513+ } ,
514+ } )
515+ . catch ( ( ) => { } )
308516 }
309517 }
310518
0 commit comments