@@ -625,6 +625,7 @@ function parseToolCalls(text) {
625625function repairToolCalls ( toolCalls , responseText ) {
626626 const repaired = [ ] ;
627627 const issues = [ ] ;
628+ const droppedFilePaths = [ ] ; // filePaths from dropped write_file (empty content) — for cross-iter fallback
628629
629630 for ( const call of toolCalls ) {
630631 if ( ! call || typeof call . tool !== 'string' ) continue ;
@@ -645,6 +646,7 @@ function repairToolCalls(toolCalls, responseText) {
645646 }
646647 // Unrecoverable — drop it instead of executing an empty write_file
647648 issues . push ( `Dropped write_file: empty content for "${ filePath || '(no path)' } " and no recoverable code found in response.` ) ;
649+ if ( filePath ) droppedFilePaths . push ( filePath ) ; // track for cross-iter fallback
648650 continue ;
649651 }
650652
@@ -704,7 +706,7 @@ function repairToolCalls(toolCalls, responseText) {
704706 console . log ( `[MCP Repair] ${ issues . length } issue(s): ${ issues . join ( ' | ' ) } ` ) ;
705707 }
706708
707- return { repaired, issues } ;
709+ return { repaired, issues, droppedFilePaths } ;
708710}
709711
710712/**
@@ -903,8 +905,10 @@ async function processResponse(responseText, options = {}) {
903905 // Fix malformed calls BEFORE execution — recover empty write_file params,
904906 // drop unrecoverable calls, fix URLs, etc. This prevents tool errors from
905907 // polluting context and confusing the model for the rest of the session.
908+ let _repairDropped = [ ] ; // filePaths dropped here — threaded to fallback and returned for next-iter
906909 if ( toolCalls . length > 0 ) {
907- const { repaired, issues } = repairToolCalls ( toolCalls , responseText ) ;
910+ const { repaired, issues, droppedFilePaths : _rd } = repairToolCalls ( toolCalls , responseText ) ;
911+ _repairDropped = _rd || [ ] ;
908912 if ( issues . length > 0 ) {
909913 console . log ( `[MCP] Repair dropped/fixed ${ issues . length } call(s)` ) ;
910914 }
@@ -953,7 +957,7 @@ async function processResponse(responseText, options = {}) {
953957 toolCalls . push ( ...proseCommands ) ;
954958 }
955959
956- const fallbackCalls = this . _detectFallbackFileOperations ( responseText , options . userMessage ) ;
960+ const fallbackCalls = this . _detectFallbackFileOperations ( responseText , options . userMessage , [ ... _repairDropped , ... ( options . lastDroppedFilePaths || [ ] ) ] ) ;
957961 if ( fallbackCalls . length > 0 ) {
958962 console . log ( '[MCP] Found fallback tool calls:' , fallbackCalls . length ) ;
959963 let effectiveFallbackCalls = fallbackCalls ;
@@ -973,10 +977,10 @@ async function processResponse(responseText, options = {}) {
973977 const result = await this . executeTool ( call . tool , call . params || { } ) ;
974978 results . push ( { tool : call . tool , params : call . params , result } ) ;
975979 }
976- return { hasToolCalls : true , results, capped : fbCapped , skippedToolCalls : fbSkipped , formalCallCount : 0 } ;
980+ return { hasToolCalls : true , results, capped : fbCapped , skippedToolCalls : fbSkipped , formalCallCount : 0 , droppedFilePaths : [ ] } ;
977981 }
978982 console . log ( '[MCP] No fallback tool calls either' ) ;
979- return { hasToolCalls : false , results : [ ] , formalCallCount : 0 } ;
983+ return { hasToolCalls : false , results : [ ] , formalCallCount : 0 , droppedFilePaths : _repairDropped } ;
980984 }
981985
982986 // ── Browser Tool Capping ──
@@ -1062,14 +1066,14 @@ async function processResponse(responseText, options = {}) {
10621066 console . log ( `[MCP] Browser cap enforced: executed ${ browserStateChanges } state-changing actions, skipped ${ browserSkipped } ` ) ;
10631067 }
10641068
1065- return { hasToolCalls : true , results, capped : capped || browserCapped , skippedToolCalls : skippedCount + browserSkipped , formalCallCount : toolCalls . length } ;
1069+ return { hasToolCalls : true , results, capped : capped || browserCapped , skippedToolCalls : skippedCount + browserSkipped , formalCallCount : toolCalls . length , droppedFilePaths : _repairDropped } ;
10661070}
10671071
10681072/**
10691073 * Fallback detection for file operations when model doesn't use formal tool syntax.
10701074 * Looks for patterns like "```html\n<!DOCTYPE...```" with context suggesting file creation.
10711075 */
1072- function _detectFallbackFileOperations ( responseText , userMessage ) {
1076+ function _detectFallbackFileOperations ( responseText , userMessage , lastDroppedFilePaths = [ ] ) {
10731077 const results = [ ] ;
10741078
10751079 // ── Phase 1: Bash/shell/cmd code blocks → run_command recovery ──
@@ -1114,7 +1118,22 @@ function _detectFallbackFileOperations(responseText, userMessage) {
11141118 // Explicit file-creation intent language is the only reliable signal.
11151119 // However, when the response is PREDOMINANTLY code blocks (>60% by character count),
11161120 // the model likely intended to create files, not explain concepts. Allow fallback in that case.
1117- if ( ! hasFileIntent && ! hasCodeBlocksWithLang ) return results ;
1121+ // Fix D: Detect large raw HTML/code blobs without backtick fences
1122+ // (e.g., models that dump HTML directly without tool structure after context rotation)
1123+ const _isLargeRawCodeBlob = ! hasCodeBlocksWithLang && responseText . length > 1500 && (
1124+ / < h t m l [ \s > ] | < ! d o c t y p e \s + h t m l / i. test ( responseText ) ||
1125+ ( responseText . length > 4000 && / < \/ h t m l \s * > / i. test ( responseText ) )
1126+ ) ;
1127+ if ( ! hasFileIntent && ! hasCodeBlocksWithLang && ! _isLargeRawCodeBlob ) return results ;
1128+ if ( ! hasFileIntent && ! hasCodeBlocksWithLang && _isLargeRawCodeBlob ) {
1129+ const _recovered = _recoverWriteFileContent ( responseText ,
1130+ lastDroppedFilePaths . length > 0 ? lastDroppedFilePaths [ 0 ] : undefined ) ;
1131+ if ( _recovered ) {
1132+ console . log ( `[MCP] Fallback: raw code blob (${ responseText . length } chars) → write_file "${ _recovered . params . filePath } "` ) ;
1133+ results . push ( _recovered ) ;
1134+ }
1135+ return results ;
1136+ }
11181137 if ( ! hasFileIntent && hasCodeBlocksWithLang ) {
11191138 // Measure code block ratio to avoid false positives on explanatory code
11201139 const _cbRegex = / ` ` ` \w + \s * \n ( [ \s \S ] * ?) ` ` ` / g;
@@ -1127,6 +1146,7 @@ function _detectFallbackFileOperations(responseText, userMessage) {
11271146
11281147 const codeBlockRegex = / ` ` ` ( \w + ) ? \s * \n ( [ \s \S ] * ?) ` ` ` / g;
11291148 let match ;
1149+ const _hintMap = { } ; // ext→filePath hints extracted from same-response write_file json headers
11301150
11311151 const filePathPatterns = [
11321152 / (?: (?: c r e a t e | w r i t e | s a v e | m a k e | g e n e r a t e ) .* ?(?: f i l e | d o c u m e n t ) .* ?(?: n a m e d ? | c a l l e d ? | a t ) ? ) \s * [ ` " ' ] ( [ ^ ` " ' \n ] + \. \w + ) [ ` " ' ] / gi,
@@ -1149,7 +1169,20 @@ function _detectFallbackFileOperations(responseText, userMessage) {
11491169 if ( ! content || content . length < 10 ) continue ;
11501170
11511171 // Skip code blocks that look like tool call JSON (already handled by parseToolCalls)
1152- if ( lang === 'json' && / ^ \s * \{ \s * [ " ' ] ? (?: t o o l | n a m e ) [ " ' ] ? \s * : / . test ( content ) ) continue ;
1172+ // But extract filePath hints from write_file/create_file headers with no content
1173+ if ( lang === 'json' && / ^ \s * \{ \s * [ " ' ] ? (?: t o o l | n a m e ) [ " ' ] ? \s * : / . test ( content ) ) {
1174+ try {
1175+ const _jp = JSON . parse ( content ) ;
1176+ const _jt = _jp . tool || _jp . name ;
1177+ const _jfp = _jp . params ?. filePath || _jp . params ?. file_path || _jp . filePath ;
1178+ if ( ( _jt === 'write_file' || _jt === 'create_file' ) && _jfp &&
1179+ ( ! _jp . params ?. content || String ( _jp . params . content ) . length < 5 ) ) {
1180+ const _jext = _jfp . includes ( '.' ) ? _jfp . split ( '.' ) . pop ( ) . toLowerCase ( ) : '' ;
1181+ if ( _jext ) _hintMap [ '.' + _jext ] = _jfp ;
1182+ }
1183+ } catch ( _e ) { /* ignore parse errors */ }
1184+ continue ;
1185+ }
11531186
11541187 if ( lang === 'python' || lang === 'py' ) {
11551188 if ( / i m p o r t o s | o p e n \s * \( | o s \. m a k e d i r s | f s \. w r i t e / . test ( content ) ) continue ;
@@ -1202,6 +1235,23 @@ function _detectFallbackFileOperations(responseText, userMessage) {
12021235 }
12031236 }
12041237
1238+ // Use hints: 1) same-response json header (_hintMap), 2) cross-iter dropped filePaths
1239+ if ( ! filePath && lang && langToExt [ lang ] && _hintMap [ langToExt [ lang ] ] ) {
1240+ filePath = _hintMap [ langToExt [ lang ] ] ;
1241+ delete _hintMap [ langToExt [ lang ] ] ; // consume hint
1242+ console . log ( `[MCP] Fallback: same-response hint "${ filePath } " for ${ lang } block` ) ;
1243+ }
1244+ if ( ! filePath && lang && langToExt [ lang ] && lastDroppedFilePaths . length > 0 ) {
1245+ const _crossFp = lastDroppedFilePaths . find ( fp => {
1246+ const _fext = fp . includes ( '.' ) ? '.' + fp . split ( '.' ) . pop ( ) . toLowerCase ( ) : '' ;
1247+ return langToExt [ lang ] === _fext ;
1248+ } ) ;
1249+ if ( _crossFp ) {
1250+ filePath = _crossFp ;
1251+ console . log ( `[MCP] Fallback: cross-iter hint "${ filePath } " for ${ lang } block` ) ;
1252+ }
1253+ }
1254+
12051255 if ( ! filePath && lang && langToExt [ lang ] ) {
12061256 const folderMatch = textBefore . match ( / (?: i n | i n s i d e | w i t h i n | t o | f o l d e r | d i r e c t o r y ) [: \s] + [ ` " ' ] ? ( [ a - z A - Z _ ] [ \w / \\ - ] * ) [ ` " ' ] ? / i) ;
12071257 const folder = folderMatch ? folderMatch [ 1 ] . replace ( / \\ / g, '/' ) : '' ;
0 commit comments