@@ -983,11 +983,31 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
983983 this . _lastAbortReason = 'timeout' ;
984984 this . cancelGeneration ( 'timeout' ) ;
985985 } , GEN_TIMEOUT_MS ) : null ;
986-
986+
987+ // Token stall watchdog — fires if no token arrives for STALL_TIMEOUT_MS.
988+ // Independent of generationTimeoutMs (total duration limit).
989+ // Catches silent GPU/CPU hangs where inference never produces a first token,
990+ // e.g., during seamless continuation under VRAM pressure after a long generation.
991+ // 90s is generous: normal first-token latency is <10s even on constrained hardware;
992+ // 90s without ANY token is unambiguously a hang, not slow generation.
993+ const STALL_TIMEOUT_MS = 90_000 ;
994+ let stallTimer = null ;
995+ const resetStallTimer = ( ) => {
996+ clearTimeout ( stallTimer ) ;
997+ stallTimer = setTimeout ( ( ) => {
998+ const elapsed = Math . round ( ( Date . now ( ) - lastTokenTime ) / 1000 ) ;
999+ console . log ( `[LLM] Token stall detected — no tokens for ${ elapsed } s — aborting generation to recover` ) ;
1000+ this . _lastAbortReason = 'timeout' ;
1001+ this . cancelGeneration ( 'timeout' ) ;
1002+ } , STALL_TIMEOUT_MS ) ;
1003+ } ;
1004+
9871005 let fullResponse = '' ;
9881006 let rawResponse = '' ;
9891007 let thinkingTokenCount = 0 ;
9901008 let lastTokenTime = Date . now ( ) ;
1009+ resetStallTimer ( ) ; // Start stall watchdog — fires if no first token within 90s
1010+ console . log ( '[LLM] Generation started' ) ;
9911011
9921012 // Thinking model state: suppress content between <think> and </think>
9931013 let insideThinkBlock = false ;
@@ -1084,6 +1104,9 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
10841104 const text = chunk . text || '' ;
10851105 if ( ! text ) return ;
10861106
1107+ // Reset stall watchdog — a token arrived, inference is alive.
1108+ resetStallTimer ( ) ;
1109+
10871110 // Keep a raw stream for internal tool detection/debug.
10881111 // IMPORTANT: do not emit raw thought to normal onToken.
10891112 rawResponse += text ;
@@ -1194,8 +1217,9 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
11941217
11951218 let result = await runOnce ( ) ;
11961219
1197- // Clear generation safety timeout — completed normally
1220+ // Clear generation safety timeout and stall watchdog — completed normally
11981221 clearTimeout ( genTimeoutTimer ) ;
1222+ clearTimeout ( stallTimer ) ;
11991223
12001224 // Generation timeout: if generation takes more than 120s, something is wrong.
12011225 // Abort and return what we have (prevents infinite hangs on CPU-bound systems).
@@ -1227,6 +1251,7 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
12271251 rawResponse = '' ;
12281252 thinkingTokenCount = 0 ;
12291253 lastTokenTime = Date . now ( ) ;
1254+ resetStallTimer ( ) ; // Restart stall watchdog for retry attempt
12301255 insideThinkBlock = false ;
12311256 tagBuffer = '' ;
12321257 toolDetectBuffer = '' ;
@@ -1266,8 +1291,9 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
12661291 stopReason : result . metadata ?. stopReason || 'eogToken' ,
12671292 } ;
12681293 } catch ( error ) {
1269- // Clear generation safety timeout on error path
1294+ // Clear generation safety timeout and stall watchdog on error path
12701295 clearTimeout ( genTimeoutTimer ) ;
1296+ clearTimeout ( stallTimer ) ;
12711297
12721298 // Invalidate KV cache — chatHistory is about to be mutated in ways that
12731299 // don't match what was evaluated, so lastEvaluation is stale
@@ -1689,8 +1715,24 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
16891715 this . cancelGeneration ( 'timeout' ) ;
16901716 } , GEN_TIMEOUT_MS ) : null ;
16911717
1718+ // Token stall watchdog — same rationale as generateStream (see that function).
1719+ const STALL_TIMEOUT_MS_FC = 90_000 ;
1720+ let lastTokenTimeFC = Date . now ( ) ;
1721+ let stallTimerFC = null ;
1722+ const resetStallTimerFC = ( ) => {
1723+ clearTimeout ( stallTimerFC ) ;
1724+ stallTimerFC = setTimeout ( ( ) => {
1725+ const elapsed = Math . round ( ( Date . now ( ) - lastTokenTimeFC ) / 1000 ) ;
1726+ console . log ( `[LLM] Token stall detected (function-calling) — no tokens for ${ elapsed } s — aborting` ) ;
1727+ this . _lastAbortReason = 'timeout' ;
1728+ this . cancelGeneration ( 'timeout' ) ;
1729+ } , STALL_TIMEOUT_MS_FC ) ;
1730+ } ;
1731+
16921732 let fullResponse = '' ;
16931733 let collectedFunctionCalls = [ ] ;
1734+ resetStallTimerFC ( ) ; // Start stall watchdog on generation begin
1735+ console . log ( '[LLM] Function-calling generation started' ) ;
16941736
16951737 try {
16961738 this . _compactHistory ( ) ;
@@ -1752,6 +1794,7 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
17521794 } ,
17531795 } : { } ) ,
17541796 onResponseChunk : ( chunk ) => {
1797+ if ( chunk . text ) { lastTokenTimeFC = Date . now ( ) ; resetStallTimerFC ( ) ; }
17551798 if ( chunk . segmentType === 'thought' ) {
17561799 if ( onThinkingToken && chunk . text ) onThinkingToken ( chunk . text ) ;
17571800 } else if ( chunk . text ) {
@@ -1812,6 +1855,7 @@ After your brief acknowledgment, output ONLY the tool call blocks — no extra t
18121855 throw error ;
18131856 } finally {
18141857 clearTimeout ( genTimeoutTimer ) ;
1858+ clearTimeout ( stallTimerFC ) ;
18151859 this . abortController = null ;
18161860 this . _abortReason = null ;
18171861 }
0 commit comments