Skip to content

Commit 4a8fd27

Browse files
author
Brendan Gray
committed
v1.7.16 - add token stall watchdog to generateStream and generateWithFunctions to prevent infinite generation hang
1 parent 6fc140f commit 4a8fd27

2 files changed

Lines changed: 48 additions & 4 deletions

File tree

main/llmEngine.js

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "guide-ide",
3-
"version": "1.7.15",
3+
"version": "1.7.16",
44
"description": "guIDE - AI-Powered Offline IDE with local LLM, RAG, MCP tools, browser automation, and integrated terminal",
55
"author": {
66
"name": "Brendan Gray",

0 commit comments

Comments
 (0)