Skip to content

Commit 77bf593

Browse files
author
Brendan Gray
committed
v1.8.13: Fix crash on undefined result, display tool results, Save as File button, continuation loop guards, JSON leak fix, preview cap fix, session null guard
- Fix bare return in handleLocalChat returning undefined (crash on .success access) - Add optional chaining + fallback on api.aiChat() result in ChatPanel - Display actual tool results (PARAMETERS + RESULT) instead of 'Completed' placeholder - Add Save as File button on code blocks (save dialog + write + open) - Replace exact-match repeat detection with similarity-based (>80% overlap) - Add 50K char hard limit and forward-progress scoring for continuation loop - Fix string-aware brace counting in splitInlineToolCalls (JSON leak fix) - Add safety net regex to strip residual JSON tool blobs from chat text - Smart paramsText truncation preserving content key visibility - Null guard before new LlamaChat when sequence is null/disposed - Pipe tool result data through tool-executing and mcp-tool-results IPC - Improve Generating spinner to show Creating {filename} with ellipsis
1 parent 876857e commit 77bf593

6 files changed

Lines changed: 186 additions & 47 deletions

File tree

main/agenticChat.js

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ function register(ctx) {
413413

414414
sendToolExecutionEvents(mainWindow, iterationToolResults, playwrightBrowser);
415415
capArray(allCloudToolResults, 50);
416-
if (mainWindow) mainWindow.webContents.send('mcp-tool-results');
416+
if (mainWindow) mainWindow.webContents.send('mcp-tool-results', iterationToolResults);
417417

418418
// Build tool feedback
419419
const toolSummaryParts = [];
@@ -505,7 +505,7 @@ function register(ctx) {
505505
if (!llmEngine.isReady) {
506506
if (mainWindow) mainWindow.webContents.send('llm-token', '\n*[Model is still loading — please wait and try again]*\n');
507507
if (mainWindow) mainWindow.webContents.send('llm-done');
508-
return;
508+
return { success: false, error: 'Model is still loading — please wait and try again' };
509509
}
510510
}
511511

@@ -543,7 +543,7 @@ function register(ctx) {
543543
done: true,
544544
});
545545
}
546-
return;
546+
return { success: false, error: `Context window too small (${totalCtx} tokens) for tool-assisted generation` };
547547
}
548548
}
549549
console.log(`[AI Chat] Model: ${modelTier.family} (${modelTier.paramLabel} ${modelTier.family}) \u2014 tools=${modelProfile.generation?.maxToolsPerTurn ?? 0}, grammar=${modelProfile.generation?.grammarConstrained ? 'strict' : 'limited'}`);
@@ -722,6 +722,7 @@ function register(ctx) {
722722
let _contLowProgressCount = 0;
723723
let _contRepeatCount = 0;
724724
let _lastContText = '';
725+
let _contCharSizes = []; // Track char counts for pattern detection
725726
let _pendingPartialBlock = null;
726727
let lastIterationResponse = '';
727728
let nonContextRetries = 0;
@@ -907,7 +908,20 @@ function register(ctx) {
907908
}
908909
if (_tStart !== -1 && _tName && mainWindow && !mainWindow.isDestroyed()) {
909910
const raw = _tb.slice(_tStart);
910-
const paramsText = raw.length > 4000 ? raw.slice(0, 4000) : raw;
911+
// Smart truncation: ensure "content" key is always visible for preview
912+
let paramsText;
913+
if (raw.length <= 8000) {
914+
paramsText = raw;
915+
} else {
916+
// Keep first 1000 chars (covers tool name, filePath) + last 4000 chars (covers content tail)
917+
const contentIdx = raw.indexOf('"content"');
918+
if (contentIdx !== -1 && contentIdx < raw.length) {
919+
// Keep from content key onward (up to 6000 chars) plus the header
920+
paramsText = raw.slice(0, Math.min(contentIdx + 6000, raw.length));
921+
} else {
922+
paramsText = raw.slice(0, 4000);
923+
}
924+
}
911925
mainWindow.webContents.send('llm-tool-generating', {
912926
callIndex: _tIdx, functionName: _tName, paramsText, done: false,
913927
});
@@ -1197,16 +1211,58 @@ function register(ctx) {
11971211
// Detect repeated identical content (model stuck in loop)
11981212
if (responseText.trim() === _lastContText.trim() && responseText.length > 0) {
11991213
_contRepeatCount++;
1214+
} else if (_lastContText.length > 0 && responseText.length > 0) {
1215+
// Similarity-based detection: if >80% of chars overlap, count as repeat
1216+
const a = responseText.trim(), b = _lastContText.trim();
1217+
const shorter = Math.min(a.length, b.length), longer = Math.max(a.length, b.length);
1218+
if (shorter > 50 && longer > 0) {
1219+
// Simple char overlap ratio: count matching chars at same positions
1220+
let matches = 0;
1221+
for (let ci = 0; ci < shorter; ci++) { if (a[ci] === b[ci]) matches++; }
1222+
const similarity = matches / longer;
1223+
if (similarity > 0.8) {
1224+
_contRepeatCount++;
1225+
console.log(`[AI Chat] Near-identical continuation detected (similarity=${(similarity * 100).toFixed(1)}%)`);
1226+
} else {
1227+
_contRepeatCount = 0;
1228+
}
1229+
} else {
1230+
_contRepeatCount = 0;
1231+
}
12001232
} else {
12011233
_contRepeatCount = 0;
12021234
}
12031235
_lastContText = responseText;
12041236

1205-
if (_contLowProgressCount >= 3 || _contRepeatCount >= 2) {
1206-
console.log(`[AI Chat] Continuation aborted: ${_contRepeatCount >= 2 ? 'repeated identical content' : 'no forward progress'}`);
1237+
// Track output sizes for pattern detection (Step 9)
1238+
_contCharSizes.push(responseText.length);
1239+
1240+
// Hard total accumulated char limit: stop runaway continuation
1241+
const MAX_CONTINUATION_CHARS = 50000;
1242+
let _contAbortReason = '';
1243+
if (fullResponseText.length > MAX_CONTINUATION_CHARS) {
1244+
_contAbortReason = `total output exceeds ${MAX_CONTINUATION_CHARS} chars`;
1245+
} else if (_contLowProgressCount >= 3) {
1246+
_contAbortReason = 'no forward progress';
1247+
} else if (_contRepeatCount >= 2) {
1248+
_contAbortReason = 'repeated/near-identical content';
1249+
}
1250+
1251+
// Forward-progress scoring: after 5 passes, if avg chars per pass varies <10% from first pass, abort
1252+
if (!_contAbortReason && _contCharSizes.length >= 5) {
1253+
const firstSize = _contCharSizes[0];
1254+
const avgSize = _contCharSizes.reduce((s, v) => s + v, 0) / _contCharSizes.length;
1255+
if (firstSize > 0 && Math.abs(avgSize - firstSize) / firstSize < 0.10) {
1256+
_contAbortReason = `uniform output size (~${Math.round(avgSize)} chars/pass for ${_contCharSizes.length} passes)`;
1257+
}
1258+
}
1259+
1260+
if (_contAbortReason) {
1261+
console.log(`[AI Chat] Continuation aborted: ${_contAbortReason}`);
12071262
continuationCount = 0;
12081263
_contLowProgressCount = 0;
12091264
_contRepeatCount = 0;
1265+
_contCharSizes = [];
12101266
// Fall through
12111267
} else {
12121268
const truncReason = _hasUnclosedToolFence ? 'unclosed fence' : 'maxTokens';

main/agenticChatHelpers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function sendToolExecutionEvents(mainWindow, toolResults, playwrightBrowser, opt
6969
let filesChanged = false;
7070

7171
for (const tr of toolResults) {
72-
mainWindow.webContents.send('tool-executing', { tool: tr.tool, params: tr.params });
72+
mainWindow.webContents.send('tool-executing', { tool: tr.tool, params: tr.params, result: tr.result });
7373
if (tr.tool?.startsWith('browser_') && !playwrightBrowser?.isLaunched) {
7474
mainWindow.webContents.send('show-browser', { url: tr.params?.url || '' });
7575
}

main/llmEngine.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,10 @@ class LLMEngine extends EventEmitter {
10841084
this.sequence = this.context.getSequence();
10851085
}
10861086

1087+
if (!this.sequence || this.sequence._disposed) {
1088+
throw new Error('Cannot reset session: sequence unavailable after all fallback attempts');
1089+
}
1090+
10871091
const llamaCppPath = this._getNodeLlamaCppPath();
10881092
const { LlamaChat } = await import(pathToFileURL(llamaCppPath).href);
10891093
this.chat = new LlamaChat({ contextSequence: this.sequence });

0 commit comments

Comments
 (0)