Skip to content

Commit 3bfd31f

Browse files
author
Brendan Gray
committed
v1.8.44: Fix 61 - Band-aid removal audit + crash fixes
- Fix buildFileProgressHint scope bug (crash after first message) - Remove code-dump nudge (regeneration loop fix) - Remove guardFirstTurnOverflow - Remove getProgressiveTools (keyword-based tool filtering) - Remove cross-turn duplicate tool call blocker - Remove count-based write blocker (keep Fix 57 disk-aware protection) - Remove prose command detection + fallback file operation detection - Remove write dedup in mcpToolServer formal tool path - Simplify detectStuckCycle to single threshold - Remove iteration gate from classifyResponseFailure - UI: Collapse Keep/Undo by default - UI: Stack consecutive identical tool calls - UI: Update recommended models to Qwen3.5 - UI: Fix text overlap on model list - UI: Fix See N more off-center - UI: Modernize settings sliders - Backend: Fix cycle detection false positives (paramsHash) - Backend: Fix context compaction destroying tool results - Backend: Add null sequence guard in llmEngine
1 parent af50be8 commit 3bfd31f

14 files changed

Lines changed: 516 additions & 644 deletions

main/agenticChat.js

Lines changed: 31 additions & 215 deletions
Large diffs are not rendered by default.

main/agenticChatHelpers.js

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -326,35 +326,14 @@ function evaluateResponse(responseText, functionCalls, taskType, iteration) {
326326
return { verdict: 'COMMIT', reason: 'default' };
327327
}
328328

329-
/**
330-
* Progressive tool disclosure — returns a filtered list of tool names
331-
* based on model tier limits.
332-
*/
333-
function getProgressiveTools(taskType, iteration, recentTools, maxTools) {
334-
if (!maxTools) return null;
335-
336-
const priorityTools = [
337-
'read_file', 'write_file', 'append_to_file', 'edit_file', 'list_directory', 'run_command',
338-
'web_search', 'search_codebase', 'grep_search', 'find_files',
339-
'browser_navigate', 'browser_snapshot', 'browser_click', 'browser_type',
340-
'browser_scroll', 'browser_press_key', 'browser_select_option',
341-
'browser_evaluate', 'browser_get_content', 'browser_screenshot',
342-
'browser_back', 'browser_hover', 'browser_tabs', 'fetch_webpage',
343-
'write_todos', 'update_todo', 'save_memory', 'get_memory',
344-
'git_status', 'git_diff', 'git_commit',
345-
'delete_file', 'rename_file', 'get_file_info', 'analyze_error',
346-
];
347-
return priorityTools.slice(0, maxTools);
348-
}
349-
350329
/**
351330
* Failure classification — only stops loop on genuine infinite repetition.
352331
*/
353332
function classifyResponseFailure(responseText, hasToolCalls, taskType, iteration, originalMessage, lastResponse, options = {}) {
354333
if (hasToolCalls) return null;
355334

356335
const text = (responseText || '').trim();
357-
if (lastResponse && text.length > 100 && iteration > 2) {
336+
if (lastResponse && text.length > 100) {
358337
if (isNearDuplicate(lastResponse, text, 0.80)) {
359338
return { type: 'repetition', severity: 'stop', recovery: { action: 'stop', prompt: '' } };
360339
}
@@ -403,13 +382,15 @@ function progressiveContextCompaction(options) {
403382
pruned += pruneVerboseHistory(chatHistory, 6);
404383
}
405384

406-
// Phase 3: Aggressive compaction
385+
// Phase 3: Aggressive compaction — protect last 4 results so model can see recent tool output
407386
if (pct > phase3Threshold) {
408-
for (let i = 0; i < allToolResults.length - 2; i++) {
387+
const protectCount = Math.min(4, allToolResults.length);
388+
for (let i = 0; i < allToolResults.length - protectCount; i++) {
409389
const tr = allToolResults[i];
410390
if (!tr.result?._pruned) {
411-
const status = tr.result?.success ? 'ok' : 'fail';
412-
tr.result = { _pruned: true, tool: tr.tool, status };
391+
const resultStr = typeof tr.result === 'string' ? tr.result : JSON.stringify(tr.result || '');
392+
const status = tr.result?.success !== false ? 'ok' : 'fail';
393+
tr.result = { _pruned: true, tool: tr.tool, status, snippet: resultStr.substring(0, 300) };
413394
pruned++;
414395
}
415396
}
@@ -831,7 +812,6 @@ module.exports = {
831812
pruneVerboseHistory,
832813
pruneCloudHistory,
833814
evaluateResponse,
834-
getProgressiveTools,
835815
classifyResponseFailure,
836816
progressiveContextCompaction,
837817
buildToolFeedback,

main/ipc/modelHandlers.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,16 @@ function register(ctx) {
5050
const maxModelGB = vramGB > 2 ? Math.max(vramGB - 1.5, 1) : totalRAM * 0.6;
5151

5252
const allModels = [
53+
{ name: 'Qwen3.5-0.6B', file: 'Qwen3.5-0.6B-Q8_0.gguf', size: 0.6, hfRepo: 'unsloth/Qwen3.5-0.6B-GGUF', desc: 'Ultra-lightweight chat model', category: 'general', vision: false },
5354
{ name: 'Qwen2.5-Coder-1.5B-Instruct', file: 'Qwen2.5-Coder-1.5B-Instruct-Q4_K_M.gguf', size: 1.0, hfRepo: 'lmstudio-community/Qwen2.5-Coder-1.5B-Instruct-GGUF', desc: 'Fast coding model, great for autocomplete', category: 'coding', vision: false },
54-
{ name: 'Qwen3-0.6B', file: 'Qwen3-0.6B-Q8_0.gguf', size: 0.6, hfRepo: 'unsloth/Qwen3-0.6B-GGUF', desc: 'Ultra-lightweight general chat model', category: 'general', vision: false },
55-
{ name: 'Qwen3-4B', file: 'Qwen3-4B-Q4_K_M.gguf', size: 2.5, hfRepo: 'lmstudio-community/Qwen3-4B-GGUF', desc: 'Fast reasoning model with thinking mode', category: 'general', vision: false },
55+
{ name: 'Qwen3.5-2B', file: 'Qwen3.5-2B-Q8_0.gguf', size: 1.9, hfRepo: 'unsloth/Qwen3.5-2B-GGUF', desc: 'Compact general model, excellent for its size', category: 'general', vision: false },
56+
{ name: 'Qwen3.5-4B', file: 'Qwen3.5-4B-Q4_K_M.gguf', size: 2.5, hfRepo: 'unsloth/Qwen3.5-4B-GGUF', desc: 'Fast reasoning model with thinking mode', category: 'general', vision: false },
5657
{ name: 'Qwen2.5-Coder-7B-Instruct', file: 'Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf', size: 4.7, hfRepo: 'lmstudio-community/Qwen2.5-Coder-7B-Instruct-GGUF', desc: 'Strong coding model, Q4 quantized', category: 'coding', vision: false },
57-
{ name: 'Llama-3.1-8B-Instruct', file: 'Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf', size: 4.9, hfRepo: 'bartowski/Meta-Llama-3.1-8B-Instruct-GGUF', desc: 'Excellent general-purpose model by Meta', category: 'general', vision: false },
58-
{ name: 'Qwen3-8B', file: 'Qwen3-8B-Q4_K_M.gguf', size: 5.0, hfRepo: 'lmstudio-community/Qwen3-8B-GGUF', desc: 'Strong reasoning model with thinking', category: 'general', vision: false },
59-
{ name: 'DeepSeek-R1-Distill-Qwen-14B', file: 'DeepSeek-R1-Distill-Qwen-14B-Q4_K_M.gguf', size: 8.7, hfRepo: 'bartowski/DeepSeek-R1-Distill-Qwen-14B-GGUF', desc: 'DeepSeek R1 reasoning distilled into 14B', category: 'reasoning', vision: false },
60-
{ name: 'Qwen3-14B', file: 'Qwen3-14B-Q4_K_M.gguf', size: 9.0, hfRepo: 'lmstudio-community/Qwen3-14B-GGUF', desc: 'High-quality reasoning model', category: 'general', vision: false },
58+
{ name: 'Qwen3.5-9B', file: 'Qwen3.5-9B-Q4_K_M.gguf', size: 5.2, hfRepo: 'unsloth/Qwen3.5-9B-GGUF', desc: 'Best quality-to-size ratio, strong reasoning', category: 'general', vision: false },
59+
{ name: 'Qwen3.5-14B', file: 'Qwen3.5-14B-Q4_K_M.gguf', size: 8.7, hfRepo: 'unsloth/Qwen3.5-14B-GGUF', desc: 'High-quality reasoning model', category: 'general', vision: false },
6160
{ name: 'Mistral-Small-3.1-24B', file: 'Mistral-Small-3.1-24B-Instruct-2503-Q4_K_M.gguf', size: 14.3, hfRepo: 'lmstudio-community/Mistral-Small-3.1-24B-Instruct-2503-GGUF', desc: 'Powerful multi-language coding + reasoning', category: 'general', vision: false },
6261
{ name: 'Qwen3-Coder-30B-A3B (MoE)', file: 'Qwen3-Coder-30B-A3B-Instruct-Q4_K_M.gguf', size: 18.6, hfRepo: 'lmstudio-community/Qwen3-Coder-30B-A3B-Instruct-GGUF', desc: 'Best coding model — only uses 3B active params (fast!)', category: 'coding', vision: false },
63-
{ name: 'Qwen3-30B-A3B (MoE)', file: 'Qwen3-30B-A3B-Q4_K_M.gguf', size: 18.6, hfRepo: 'lmstudio-community/Qwen3-30B-A3B-GGUF', desc: 'Best general model — MoE, fast + smart', category: 'general', vision: false },
64-
{ name: 'Qwen3-32B', file: 'Qwen3-32B-Q4_K_M.gguf', size: 19.8, hfRepo: 'lmstudio-community/Qwen3-32B-GGUF', desc: 'Top-tier reasoning, dense 32B', category: 'general', vision: false },
62+
{ name: 'Qwen3.5-32B', file: 'Qwen3.5-32B-Q4_K_M.gguf', size: 19.8, hfRepo: 'unsloth/Qwen3.5-32B-GGUF', desc: 'Top-tier reasoning, dense 32B', category: 'general', vision: false },
6563
];
6664

6765
const recommended = [];

main/llmEngine.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,25 @@ class LLMEngine extends EventEmitter {
844844
if (!useKvCache && this.sequence && this.sequence.nextTokenIndex > 0) {
845845
try { this.chat?.dispose?.(); } catch {}
846846
try { this.sequence.dispose?.(); } catch {}
847-
this.sequence = this.context.getSequence();
847+
this.sequence = null;
848+
try {
849+
this.sequence = this.context.getSequence();
850+
} catch (seqErr) {
851+
const log = require('./logger');
852+
log.warn(`[_runGeneration] getSequence failed: ${seqErr.message} — recreating context`);
853+
try { this.context.dispose?.(); } catch {}
854+
const gpuIsActive = this.modelInfo && this.modelInfo.gpuMode !== false;
855+
const ctxSize = gpuIsActive
856+
? this._computeGpuContextSize({ vramGB: this._cachedVramGB || 0, modelSizeGB: this.modelInfo?.modelSizeGB || 0 })
857+
: this._computeMaxContext(this.modelInfo?.modelSizeGB || 0);
858+
this.context = await this.model.createContext({
859+
contextSize: ctxSize,
860+
flashAttention: gpuIsActive,
861+
ignoreMemorySafetyChecks: true,
862+
failedCreationRemedy: { retries: 8, autoContextSizeShrink: 0.5 },
863+
});
864+
this.sequence = this.context.getSequence();
865+
}
848866
const llamaCppPath = this._getNodeLlamaCppPath();
849867
const { LlamaChat } = await import(pathToFileURL(llamaCppPath).href);
850868
this.chat = new LlamaChat({ contextSequence: this.sequence });

main/mcpToolServer.js

Lines changed: 4 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ const {
2020
parseToolCalls: standaloneParseToolCalls,
2121
repairToolCalls,
2222
_recoverWriteFileContent,
23-
_detectProseCommands,
24-
_detectFallbackFileOperations: standaloneFallbackDetect,
2523
TOOL_NAME_ALIASES,
2624
VALID_TOOLS,
2725
} = require('./tools/toolParser');
@@ -2615,83 +2613,11 @@ class MCPToolServer {
26152613
console.log(`[MCP] Capped tool calls: executing ${maxToolsPerResponse}, skipping ${skippedCount}`);
26162614
}
26172615

2618-
// Fallback detection if no formal tool calls
2616+
// No formal tool calls found — return without attempting fallback detection.
2617+
// The model should use proper tool call format (native functions or JSON fences).
2618+
// Removed: prose command detection and fallback file operation classification.
26192619
if (toolCalls.length === 0) {
2620-
console.log('[MCP] No formal tool calls found, trying fallback detection...');
2621-
2622-
const proseCommands = _detectProseCommands(responseText);
2623-
if (proseCommands.length > 0) {
2624-
console.log('[MCP] Found prose command fallback:', proseCommands.length);
2625-
toolCalls.push(...proseCommands);
2626-
}
2627-
2628-
const fallbackCalls = this._detectFallbackFileOperations(responseText, options.userMessage, [..._repairDropped, ...(options.lastDroppedFilePaths || [])]);
2629-
if (fallbackCalls.length > 0) {
2630-
console.log('[MCP] Found fallback tool calls:', fallbackCalls.length);
2631-
let effectiveFallbackCalls = fallbackCalls;
2632-
let fbCapped = false;
2633-
let fbSkipped = 0;
2634-
if (maxToolsPerResponse > 0 && fallbackCalls.length > maxToolsPerResponse) {
2635-
fbSkipped = fallbackCalls.length - maxToolsPerResponse;
2636-
effectiveFallbackCalls = fallbackCalls.slice(0, maxToolsPerResponse);
2637-
fbCapped = true;
2638-
}
2639-
const results = [];
2640-
for (const call of effectiveFallbackCalls) {
2641-
if (toolPaceMs > 0 && results.length > 0) {
2642-
await new Promise(r => setTimeout(r, toolPaceMs));
2643-
}
2644-
if (options.writeFileHistory && call.tool === 'write_file') {
2645-
const wfPath = call.params?.filePath || call.params?.path || call.params?.file_path;
2646-
const wfLimit = (options.continuationCount || 0) > 0 ? 5 : 6;
2647-
if (wfPath && options.writeFileHistory[wfPath] && options.writeFileHistory[wfPath].count >= wfLimit) {
2648-
console.log(`[MCP] Write dedup: blocking ${call.tool} to "${wfPath}" (already written ${options.writeFileHistory[wfPath].count}x)`);
2649-
let autoConverted = false;
2650-
const newContent = call.params?.content || '';
2651-
if (newContent.length > 50) {
2652-
try {
2653-
const _fs = require('fs'), _path = require('path');
2654-
const fullPath = _path.resolve(this.projectPath || '.', wfPath);
2655-
const existing = _fs.existsSync(fullPath) ? _fs.readFileSync(fullPath, 'utf-8') : '';
2656-
if (existing.length > 0) {
2657-
const extracted = this._extractNewContentForAutoConvert(existing, newContent);
2658-
if (extracted) {
2659-
console.log(`[MCP] Write dedup auto-convert (${extracted.method}): "${wfPath}" (${extracted.overlapLines} overlap lines)`);
2660-
const ar = await this.executeTool('append_to_file', { filePath: wfPath, content: extracted.newContent });
2661-
results.push({ tool: 'append_to_file', params: { filePath: wfPath, content: '...(auto-converted)' }, result: ar });
2662-
autoConverted = true;
2663-
}
2664-
if (!autoConverted) {
2665-
// No extractable new content — file content is a subset or duplicate
2666-
results.push({ tool: call.tool, params: call.params, result: { success: true, message: `File "${wfPath}" already has this content (${existing.split('\n').length} lines). Use append_to_file to add new content, or move on to the next task.` } });
2667-
autoConverted = true;
2668-
}
2669-
}
2670-
} catch (e) { console.warn(`[MCP] Write dedup auto-convert failed: ${e.message}`); }
2671-
}
2672-
if (!autoConverted) {
2673-
// Include file tail so model knows where to append from
2674-
let fileTailHint = '';
2675-
try {
2676-
const _fs2 = require('fs'), _path2 = require('path');
2677-
const fp = _path2.resolve(this.projectPath || '.', wfPath);
2678-
if (_fs2.existsSync(fp)) {
2679-
const lines = _fs2.readFileSync(fp, 'utf-8').split('\n');
2680-
const tail = lines.slice(-10).join('\n');
2681-
fileTailHint = ` The file currently has ${lines.length} lines. Last 10 lines:\n${tail}\nUse append_to_file with filePath="${wfPath}" to continue from here.`;
2682-
}
2683-
} catch (_) {}
2684-
results.push({ tool: call.tool, params: call.params, result: { success: false, error: `BLOCKED: "${wfPath}" already written ${options.writeFileHistory[wfPath].count} times.${fileTailHint || ' Use append_to_file or edit_file instead.'}` } });
2685-
}
2686-
continue;
2687-
}
2688-
}
2689-
const result = await this.executeTool(call.tool, call.params || {});
2690-
results.push({ tool: call.tool, params: call.params, result });
2691-
}
2692-
return { hasToolCalls: true, results, capped: fbCapped, skippedToolCalls: fbSkipped, formalCallCount: 0, droppedFilePaths: [] };
2693-
}
2694-
console.log('[MCP] No fallback tool calls either');
2620+
console.log('[MCP] No formal tool calls found');
26952621
return { hasToolCalls: false, results: [], formalCallCount: 0, droppedFilePaths: _repairDropped };
26962622
}
26972623

@@ -2750,49 +2676,6 @@ class MCPToolServer {
27502676
if (call.tool.startsWith('browser_')) call.params = this._normalizeBrowserParams(call.tool, call.params || {});
27512677
else call.params = this._normalizeFsParams(call.tool, call.params || {});
27522678
}
2753-
if (options.writeFileHistory && call.tool === 'write_file') {
2754-
const wfPath = call.params?.filePath || call.params?.path || call.params?.file_path;
2755-
const wfLimit = (options.continuationCount || 0) > 0 ? 5 : 6;
2756-
if (wfPath && options.writeFileHistory[wfPath] && options.writeFileHistory[wfPath].count >= wfLimit) {
2757-
console.log(`[MCP] Write dedup: blocking ${call.tool} to "${wfPath}" (already written ${options.writeFileHistory[wfPath].count}x)`);
2758-
let autoConverted = false;
2759-
const newContent = call.params?.content || '';
2760-
if (newContent.length > 50) {
2761-
try {
2762-
const _fs = require('fs'), _path = require('path');
2763-
const fullPath = _path.resolve(this.projectPath || '.', wfPath);
2764-
const existing = _fs.existsSync(fullPath) ? _fs.readFileSync(fullPath, 'utf-8') : '';
2765-
if (existing.length > 0) {
2766-
const extracted = this._extractNewContentForAutoConvert(existing, newContent);
2767-
if (extracted) {
2768-
console.log(`[MCP] Write dedup auto-convert (${extracted.method}): "${wfPath}" (${extracted.overlapLines} overlap lines)`);
2769-
const ar = await this.executeTool('append_to_file', { filePath: wfPath, content: extracted.newContent });
2770-
results.push({ tool: 'append_to_file', params: { filePath: wfPath, content: '...(auto-converted)' }, result: ar });
2771-
autoConverted = true;
2772-
}
2773-
if (!autoConverted) {
2774-
results.push({ tool: call.tool, params: call.params, result: { success: true, message: `File "${wfPath}" already has this content (${existing.split('\n').length} lines). Use append_to_file to add new content, or move on to the next task.` } });
2775-
autoConverted = true;
2776-
}
2777-
}
2778-
} catch (e) { console.warn(`[MCP] Write dedup auto-convert failed: ${e.message}`); }
2779-
}
2780-
if (!autoConverted) {
2781-
let fileTailHint = '';
2782-
try {
2783-
const _fs2 = require('fs'), _path2 = require('path');
2784-
const fp = _path2.resolve(this.projectPath || '.', wfPath);
2785-
if (_fs2.existsSync(fp)) {
2786-
const lines = _fs2.readFileSync(fp, 'utf-8').split('\n');
2787-
const tail = lines.slice(-10).join('\n');
2788-
fileTailHint = ` The file currently has ${lines.length} lines. Last 10 lines:\n${tail}\nUse append_to_file with filePath="${wfPath}" to continue from here.`;
2789-
}
2790-
} catch (_) {}
2791-
results.push({ tool: call.tool, params: call.params, result: { success: false, error: `BLOCKED: "${wfPath}" already written ${options.writeFileHistory[wfPath].count} times.${fileTailHint || ' Use append_to_file or edit_file instead.'}` } });
2792-
}
2793-
continue;
2794-
}
2795-
}
27962679
const result = await this.executeTool(call.tool, call.params || {});
27972680
console.log('[MCP] Executed tool:', call.tool, 'result:', result.success ? 'success' : 'failed');
27982681
results.push({ tool: call.tool, params: call.params, result });
@@ -2809,10 +2692,6 @@ class MCPToolServer {
28092692
return { hasToolCalls: true, results, capped: capped || browserCapped, skippedToolCalls: skippedCount + browserSkipped, formalCallCount: toolCalls.length, droppedFilePaths: _repairDropped };
28102693
}
28112694

2812-
_detectFallbackFileOperations(responseText, userMessage, lastDroppedFilePaths = []) {
2813-
return standaloneFallbackDetect(responseText, userMessage, lastDroppedFilePaths);
2814-
}
2815-
28162695
// ─── Tool Prompt Building ────────────────────────────────────────────────
28172696

28182697
getToolPrompt() {

0 commit comments

Comments
 (0)