@@ -17,9 +17,14 @@ function checkFileCompleteness(content, filePath) {
1717 const trimmedEnd = content . trimEnd ( ) ;
1818 const lastCodeLine = trimmedEnd . split ( '\n' ) . pop ( ) . trim ( ) ;
1919 const ext = ( filePath ?. match ( / \. ( [ ^ . ] + ) $ / ) || [ ] ) [ 1 ] || '' ;
20+ // Non-code files (markdown, text, json, yaml, env, etc.) have no structural close tag.
21+ // They are always considered "complete" — no forced continuation for these types.
22+ if ( / ^ ( m d | t x t | j s o n | y a ? m l | t o m l | e n v | g i t i g n o r e | c s v | t s v | l o g | i n i | c f g | c o n f | x m l | s v g | l o c k ) $ / i. test ( ext ) ) {
23+ return true ;
24+ }
2025 let looksComplete = false ;
2126 if ( / ^ h t m l ? $ / i. test ( ext ) ) {
22- // Fix 42: Anchor to end of content — </html> must be near the end, not just anywhere in the string.
27+ // Anchor to end of content — </html> must be near the end, not just anywhere in the string.
2328 // Without $, a </html> inside a JS string/template in the middle of the file triggers a false positive.
2429 looksComplete = / < \/ h t m l \s * > \s * $ / i. test ( trimmedEnd ) ;
2530 } else if ( / ^ c s s $ / i. test ( ext ) ) {
@@ -226,8 +231,27 @@ function enrichErrorFeedback(toolName, error, failCounts = {}) {
226231 return tips . length > 0 ? `\nSuggestion: ${ tips [ 0 ] } ` : '' ;
227232}
228233
234+ /**
235+ * Compress a single text string: prune large code fences and page snapshots.
236+ * Returns the compressed string, or the original if compression didn't achieve ≥30% reduction.
237+ */
238+ function _compressText ( text ) {
239+ if ( ! text || text . length < 800 ) return null ;
240+ let compressed = text ;
241+ compressed = compressed . replace (
242+ / ` ` ` [ \s \S ] { 800 , } ?` ` ` / g,
243+ ( match ) => `\`\`\`\n[${ ( match . match ( / \n / g) || [ ] ) . length } lines — pruned]\n\`\`\``
244+ ) ;
245+ compressed = compressed . replace (
246+ / \* \* P a g e S n a p s h o t \* \* \s * \( [ ^ ) ] * \) : \n [ \s \S ] { 500 , } ?(? = \n \* \* | \n # # # | \n - - - | $ ) / g,
247+ ( ) => `**Page Snapshot**: [pruned for context]`
248+ ) ;
249+ return compressed . length < text . length * 0.7 ? compressed : null ;
250+ }
251+
229252/**
230253 * Prune verbose messages in chat history to free context space.
254+ * Handles both local format ({ type, text, response[] }) and cloud format ({ content }).
231255 */
232256function pruneVerboseHistory ( chatHistory , keepRecentCount = 6 ) {
233257 if ( ! Array . isArray ( chatHistory ) || chatHistory . length <= keepRecentCount + 1 ) return 0 ;
@@ -239,72 +263,39 @@ function pruneVerboseHistory(chatHistory, keepRecentCount = 6) {
239263 const msg = chatHistory [ i ] ;
240264 if ( ! msg ) continue ;
241265
242- // Handle model responses: { type: 'model', response: [text] }
266+ // Local format: model responses with response[] array
243267 if ( msg . type === 'model' && Array . isArray ( msg . response ) ) {
244268 let changed = false ;
245269 for ( let ri = 0 ; ri < msg . response . length ; ri ++ ) {
246- const r = msg . response [ ri ] ;
247- if ( typeof r !== 'string' || r . length < 800 ) continue ;
248- let compressed = r ;
249- compressed = compressed . replace (
250- / ` ` ` [ \s \S ] { 800 , } ?` ` ` / g,
251- ( match ) => `\`\`\`\n[${ ( match . match ( / \n / g) || [ ] ) . length } lines — pruned]\n\`\`\``
252- ) ;
253- if ( compressed . length < r . length * 0.7 ) {
254- msg . response [ ri ] = compressed ;
255- changed = true ;
256- }
270+ const compressed = _compressText ( msg . response [ ri ] ) ;
271+ if ( compressed ) { msg . response [ ri ] = compressed ; changed = true ; }
257272 }
258273 if ( changed ) pruned ++ ;
259274 continue ;
260275 }
261276
262- // Handle user/system messages: { type: 'user'|'system', text: '...' }
263- if ( ! msg . text || msg . text . length < 800 ) continue ;
264-
265- let compressed = msg . text ;
266- compressed = compressed . replace (
267- / ` ` ` [ \s \S ] { 800 , } ?` ` ` / g,
268- ( match ) => `\`\`\`\n[${ ( match . match ( / \n / g) || [ ] ) . length } lines — pruned]\n\`\`\``
269- ) ;
270- compressed = compressed . replace (
271- / \* \* P a g e S n a p s h o t \* \* \s * \( [ ^ ) ] * \) : \n [ \s \S ] { 500 , } ?(? = \n \* \* | \n # # # | \n - - - | $ ) / g,
272- ( match ) => `**Page Snapshot**: [pruned for context]`
273- ) ;
274-
275- if ( compressed . length < msg . text . length * 0.7 ) {
276- chatHistory [ i ] = { ...msg , text : compressed } ;
277- pruned ++ ;
277+ // Local format: user/system messages with text field
278+ if ( msg . text ) {
279+ const compressed = _compressText ( msg . text ) ;
280+ if ( compressed ) { chatHistory [ i ] = { ...msg , text : compressed } ; pruned ++ ; }
281+ continue ;
282+ }
283+
284+ // Cloud format: messages with content field
285+ if ( msg . content ) {
286+ const compressed = _compressText ( msg . content ) ;
287+ if ( compressed ) { chatHistory [ i ] = { ...msg , content : compressed } ; pruned ++ ; }
278288 }
279289 }
280290 return pruned ;
281291}
282292
283293/**
284294 * Prune verbose messages in cloud conversation history.
295+ * Delegates to the unified pruneVerboseHistory.
285296 */
286297function pruneCloudHistory ( history , keepRecentCount = 6 ) {
287- if ( ! Array . isArray ( history ) || history . length <= keepRecentCount + 1 ) return 0 ;
288-
289- let pruned = 0 ;
290- const cutoff = history . length - keepRecentCount ;
291-
292- for ( let i = 1 ; i < cutoff ; i ++ ) {
293- const msg = history [ i ] ;
294- if ( ! msg || ! msg . content || msg . content . length < 800 ) continue ;
295-
296- let compressed = msg . content ;
297- compressed = compressed . replace (
298- / ` ` ` [ \s \S ] { 800 , } ?` ` ` / g,
299- ( match ) => `\`\`\`\n[${ ( match . match ( / \n / g) || [ ] ) . length } lines — pruned]\n\`\`\``
300- ) ;
301-
302- if ( compressed . length < msg . content . length * 0.7 ) {
303- history [ i ] = { ...msg , content : compressed } ;
304- pruned ++ ;
305- }
306- }
307- return pruned ;
298+ return pruneVerboseHistory ( history , keepRecentCount ) ;
308299}
309300
310301/**
@@ -458,7 +449,7 @@ function formatSuccessfulToolResult(tr, opts = {}) {
458449 case 'read_file' :
459450 text += `**File:** ${ tr . params ?. filePath } ${ tr . result . readRange ? ` (lines ${ tr . result . readRange } )` : '' } \n` ;
460451 {
461- // Fix 58B: Show head+tail for large files so the model can see both
452+ // Show head+tail for large files so the model can see both
462453 // the file structure AND where it left off (critical for append workflows)
463454 const content = tr . result . content || '' ;
464455 if ( content . length > 4000 ) {
@@ -510,7 +501,7 @@ function formatSuccessfulToolResult(tr, opts = {}) {
510501 }
511502 }
512503
513- // Fix 59C: Post-write structural validation — immediate feedback loop
504+ // Post-write structural validation — immediate feedback loop
514505 // Provides IDE-level diagnostics (like LSP Problems panel) so the model
515506 // knows the structural state of the file RIGHT AFTER writing, not just at rotation.
516507 const writtenFilePath = tr . result ?. path || tr . params ?. filePath || '' ;
0 commit comments