Skip to content

Commit fa8d83b

Browse files
author
Brendan Gray
committed
v1.8.11: Fix continuation overflow, tool parse failure, UI JSON leak
- Dynamic continuation tail sized to remaining context (500-3000 chars) - Cap effectiveMaxTokens on continuation passes (70% of remaining) - Overlap de-duplication when stitching continuations - Fix sanitizeJson: escape literal newlines instead of keeping them raw - Cap IPC paramsText to 4000 chars for UI display - Frontend: show 'Generating...' spinner instead of raw JSON fallback
1 parent 3d6abbc commit fa8d83b

3 files changed

Lines changed: 35 additions & 10 deletions

File tree

main/agenticChat.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,7 @@ function register(ctx) {
883883
}
884884
if (_tStart !== -1 && _tName && mainWindow && !mainWindow.isDestroyed()) {
885885
const raw = _tb.slice(_tStart);
886-
const paramsText = raw;
886+
const paramsText = raw.length > 4000 ? raw.slice(-4000) : raw;
887887
mainWindow.webContents.send('llm-tool-generating', {
888888
callIndex: _tIdx, functionName: _tName, paramsText, done: false,
889889
});
@@ -1080,7 +1080,19 @@ function register(ctx) {
10801080
displayResponseText += displayChunk;
10811081

10821082
// ── SEAMLESS CONTINUATION ──
1083-
const _stitchedForMcp = _pendingPartialBlock ? _pendingPartialBlock + responseText : responseText;
1083+
let _stitchedForMcp;
1084+
if (_pendingPartialBlock) {
1085+
// Overlap de-duplication: detect if model repeated the tail we sent
1086+
let overlap = 0;
1087+
const maxCheck = Math.min(_pendingPartialBlock.length, responseText.length, 2000);
1088+
for (let len = maxCheck; len >= 20; len--) {
1089+
const suffix = _pendingPartialBlock.slice(-len);
1090+
if (responseText.startsWith(suffix)) { overlap = len; break; }
1091+
}
1092+
_stitchedForMcp = _pendingPartialBlock + responseText.slice(overlap);
1093+
} else {
1094+
_stitchedForMcp = responseText;
1095+
}
10841096
_pendingPartialBlock = null;
10851097
const _fenceIdx = _stitchedForMcp.search(/```(?:json|tool_call|tool)\b/);
10861098
const _afterFence = _fenceIdx !== -1 ? _stitchedForMcp.slice(_fenceIdx) : '';
@@ -1162,19 +1174,27 @@ function register(ctx) {
11621174
console.log(`[AI Chat] Seamless continuation ${continuationCount}/50 — ${truncReason} (${responseText.length} chars this pass, ${fullResponseText.length} total)`);
11631175
iteration--;
11641176

1177+
// Cap effectiveMaxTokens on continuation passes to avoid overflowing context
1178+
const remainingTokens = Math.max(0, totalCtx - Math.ceil(((typeof currentPrompt === 'string' ? currentPrompt.length : ((currentPrompt.systemContext || '').length + (currentPrompt.userMessage || '').length)) + fullResponseText.length) / 4));
1179+
effectiveMaxTokens = Math.min(effectiveMaxTokens, Math.max(256, Math.floor(remainingTokens * 0.70)));
1180+
11651181
let continuationMsg;
11661182
const taskHint = message ? `[Task: ${message.substring(0, 100)}${message.length > 100 ? '...' : ''}]\n` : '';
11671183
// Include written-files manifest so model knows what already exists
11681184
const writtenPaths = Object.keys(writeFileHistory).filter(k => writeFileHistory[k].count > 0);
11691185
const fileManifest = writtenPaths.length > 0
11701186
? `[Files already written this turn: ${writtenPaths.join(', ')}. Do NOT write to these files again. Use append_to_file or edit_file if changes needed.]\n`
11711187
: '';
1188+
// Dynamic tail size: scale with remaining context, clamped to [500, 3000] chars
1189+
const maxTailChars = Math.max(500, Math.min(Math.floor(remainingTokens * 0.3 * 4), 3000));
11721190
if (_hasUnclosedToolFence) {
11731191
const partialFence = _stitchedForMcp.slice(_fenceIdx);
1174-
_pendingPartialBlock = partialFence;
1175-
continuationMsg = `${taskHint}${fileManifest}[Continue the tool call JSON from exactly where it was cut. Output ONLY the JSON continuation. Do NOT restart the tool call. Continue from:\n${partialFence}]`;
1192+
_pendingPartialBlock = partialFence; // keep FULL text for stitching
1193+
const tailForModel = partialFence.length > maxTailChars ? partialFence.slice(-maxTailChars) : partialFence;
1194+
continuationMsg = `${taskHint}${fileManifest}[Continue the tool call JSON from exactly where it was cut. Output ONLY the JSON continuation. Do NOT restart the tool call. Continue from:\n${tailForModel}]`;
11761195
} else {
1177-
continuationMsg = `${taskHint}${fileManifest}[Continue your response exactly where you left off. Do not restart or repeat content. Here is the end of what you wrote:\n${responseText}]`;
1196+
const tailForModel = responseText.length > maxTailChars ? responseText.slice(-maxTailChars) : responseText;
1197+
continuationMsg = `${taskHint}${fileManifest}[Continue your response exactly where you left off. Do not restart or repeat content. Here is the end of what you wrote:\n${tailForModel}]`;
11781198
}
11791199

11801200
currentPrompt = {

main/tools/toolParser.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,10 @@ function sanitizeJson(raw) {
103103
if (ch === '"' && !escaped) {
104104
inStr = !inStr;
105105
}
106-
// Strip raw control chars inside strings (except tab, newline)
107-
if (inStr && ch.charCodeAt(0) < 32 && ch !== '\t' && ch !== '\n') {
106+
// Escape raw control chars inside strings (literal newlines/carriage returns break JSON.parse)
107+
if (inStr && ch.charCodeAt(0) < 32 && ch !== '\t') {
108+
if (ch === '\n') { result += '\\n'; continue; }
109+
if (ch === '\r') { result += '\\r'; continue; }
108110
result += ' ';
109111
continue;
110112
}

src/components/Chat/ChatPanel.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2668,9 +2668,12 @@ ${e.message}`,
26682668
</div>
26692669
{partialContent ? (
26702670
<CodeBlock code={partialContent} language={lang} onApply={() => {}} isToolCall={true} />
2671-
) : tc.paramsText ? (
2672-
<pre className="whitespace-pre-wrap text-[11px] font-mono text-[#d4d4d4] bg-[#1e1e1e] rounded-md p-2 max-h-[300px] overflow-y-auto">{tc.paramsText}</pre>
2673-
) : null}
2671+
) : (
2672+
<div className="flex items-center gap-2 px-2 py-1.5 bg-[#1e1e1e] rounded-md">
2673+
<Loader2 size={10} className="animate-spin text-[#007acc]" />
2674+
<span className="text-[11px] text-[#858585]">Generating {fname || 'file'}</span>
2675+
</div>
2676+
)}
26742677
</div>
26752678
);
26762679
})}

0 commit comments

Comments
 (0)