Skip to content

Commit e361c36

Browse files
author
Brendan Gray
committed
v1.8.31: Continuation rotation, cwd validation, directory deletion fixes
Fix 1: Continuation budget triggers context rotation instead of abort - When continuation hits budget limit, now rotates context with summary - Prevents mid-generation cutoffs that caused naked code blocks Fix 2: run_command validates cwd parameter - Rejects wildcards, empty strings, and invalid paths - Returns proper error instead of false success Fix 3: delete_file handles directories - Uses fs.rm with recursive:true for directories - Single tool works for both files and directories Fix 4: Preamble strengthened - Added verification requirement even after prior failures - Added directory empty claim requires list_directory - Updated delete_file instructions
1 parent 2f4928f commit e361c36

5 files changed

Lines changed: 102 additions & 22 deletions

File tree

main/agenticChat.js

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,9 +1394,6 @@ function register(ctx) {
13941394
let _hasUnclosedToolFence = _fenceIdx !== -1 &&
13951395
!_afterFence.match(/```(?:json|tool_call|tool)\b[\s\S]*?\n```/) &&
13961396
!_afterFence.match(/```(?:json|tool_call|tool)\b[\s\S]*?[^`]```\s*$/);
1397-
// Also detect any unclosed code fence (html, js, css, etc.) for higher continuation budget
1398-
const _anyCodeFenceIdx = _stitchedForMcp.search(/```(?:html?|css|javascript|js|typescript|ts|python|py|json|jsx|tsx|java|c|cpp|csharp|ruby|go|rust|php|sql|xml|yaml|sh|bash|markdown|md)\b/);
1399-
const _hasUnclosedCodeFence = _anyCodeFenceIdx !== -1 && !_stitchedForMcp.slice(_anyCodeFenceIdx).match(/```[^\n]*\n[\s\S]*?\n```/);
14001397

14011398
// If the unclosed fence contains a complete JSON tool call, don't treat as truncated
14021399
if (_hasUnclosedToolFence) {
@@ -1430,16 +1427,59 @@ function register(ctx) {
14301427
contContextPct = contUsed / totalCtx;
14311428
} catch (_) {}
14321429

1433-
// Higher budget limit for unclosed code fences to allow large code blocks to complete
1434-
const budgetLimit = _hasUnclosedToolFence ? 0.92 : (_hasUnclosedCodeFence ? 0.90 : 0.88);
1430+
const budgetLimit = _hasUnclosedToolFence ? 0.92 : 0.70;
14351431
if (contContextPct > budgetLimit) {
1436-
console.log(`[AI Chat] Continuation aborted: context at ${Math.round(contContextPct * 100)}% (limit=${Math.round(budgetLimit * 100)}%)`);
1432+
// Context budget exceeded — trigger rotation instead of aborting
1433+
// This allows large content generation (HTML, code files) to complete
1434+
console.log(`[AI Chat] Continuation budget rotation: context at ${Math.round(contContextPct * 100)}% (limit=${Math.round(budgetLimit * 100)}%)`);
14371435
continuationCount = 0;
1438-
// Try salvage
1436+
1437+
// Attempt context rotation to continue the task
1438+
if (contextRotations < MAX_CONTEXT_ROTATIONS) {
1439+
console.log(`[AI Chat] Budget-triggered rotation (${contextRotations + 1}/${MAX_CONTEXT_ROTATIONS})`);
1440+
contextRotations++;
1441+
try {
1442+
summarizer.markRotation();
1443+
const convSummary = summarizer.generateSummary({
1444+
maxTokens: Math.min(Math.floor(totalCtx * 0.25), 3000),
1445+
activeTodos: mcpToolServer?._todos || [],
1446+
});
1447+
1448+
// Store partial output for context
1449+
const partialOutput = fullResponseText.slice(-Math.min(fullResponseText.length, 2000));
1450+
await llmEngine.resetSession(true);
1451+
await ensureLlmChat(llmEngine, getNodeLlamaCppPath);
1452+
if (mainWindow && !mainWindow.isDestroyed()) {
1453+
mainWindow.webContents.send('llm-thinking-token', '\n[Context rotated to continue generation]\n');
1454+
}
1455+
1456+
// Build continuation prompt with summary and partial output
1457+
const incrementalHint = summarizer.incrementalTask
1458+
? `\n**INCREMENTAL TASK: ${summarizer.incrementalTask.current}/${summarizer.incrementalTask.target} ${summarizer.incrementalTask.type} completed.**`
1459+
: '';
1460+
const fileProgressHint = Object.keys(summarizer.fileProgress).length > 0
1461+
? `\n**FILES IN PROGRESS:** ${Object.entries(summarizer.fileProgress).map(([f, p]) => `${f} (${p.writtenLines} lines)`).join(', ')}`
1462+
: '';
1463+
1464+
currentPrompt = {
1465+
systemContext: buildStaticPrompt(),
1466+
userMessage: buildDynamicContext() + '\n\n' + convSummary +
1467+
`\n\n## CONTINUE FROM HERE\n---\n${partialOutput}\n---` +
1468+
incrementalHint + fileProgressHint +
1469+
`\n\n**CRITICAL: DO NOT REFUSE. DO NOT SAY "I cannot continue."**` +
1470+
`\nUse append_to_file to add more content. Call a tool NOW to make progress.`,
1471+
};
1472+
sessionJustRotated = true;
1473+
continue;
1474+
} catch (rotErr) {
1475+
console.error('[AI Chat] Budget-triggered rotation failed:', rotErr.message);
1476+
}
1477+
}
1478+
1479+
// Rotation failed or exhausted — try salvage as fallback
14391480
if (_hasUnclosedToolFence && _stitchedForMcp) {
14401481
const salvageResult = salvagePartialToolCall(_stitchedForMcp, _fenceIdx);
14411482
if (salvageResult) {
1442-
// Replace responseText with reconstructed tool call
14431483
fullResponseText = fullResponseText.slice(0, fullResponseText.length - (result.text || '').length) + salvageResult;
14441484
}
14451485
}

main/constants.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,10 @@ const DEFAULT_COMPACT_PREAMBLE = `You are a helpful, knowledgeable AI assistant.
105105
## Rules
106106
- **Never output full file content as code blocks in chat** — always use write_file, edit_file, or append_to_file. Code blocks are only for brief snippets or explanations.
107107
- **For new files: call write_file immediately.** Do not describe what the file would contain — create it.
108-
- **When the user asks for confirmation or verification, ALWAYS call list_directory or read_file to verify.** NEVER say "I can confirm" without actually checking. NEVER refuse a verification request — you MUST call the tool.
109-
- **Path awareness:** All relative paths are relative to the project root. Use paths like "file.html" for root files, "subfolder/file.html" for nested files. To delete directories, use run_command with "Remove-Item -Recurse -Force path".
108+
- **When the user asks for confirmation or verification, ALWAYS call list_directory or read_file to verify.** NEVER say "I can confirm" without actually checking. NEVER refuse a verification request — you MUST call the tool. Even if previous operations failed, you MUST still verify when asked.
109+
- **Never claim a directory is empty without calling list_directory.** If list_directory returns items, report them exactly as returned.
110+
- **Path awareness:** All relative paths are relative to the project root. Use paths like "file.html" for root files, "subfolder/file.html" for nested files.
111+
- **delete_file works on BOTH files AND directories.** Use delete_file for any deletion — it handles recursive directory removal automatically.
110112
- When calling tools, format tool calls as valid JSON with properly quoted string values. Never use backtick template literals in tool call JSON.
111113
- Tools execute in the live environment. Call them — do not describe what you would do.
112114
- Never say you did something unless you called the tool that did it.

main/mcpToolServer.js

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -553,9 +553,9 @@ class MCPToolServer {
553553
},
554554
{
555555
name: 'delete_file',
556-
description: 'Delete a file from the project.',
556+
description: 'Delete a file OR directory from the project. Directories are removed recursively.',
557557
parameters: {
558-
filePath: { type: 'string', description: 'Path of the file to delete', required: true },
558+
filePath: { type: 'string', description: 'Path of the file or directory to delete', required: true },
559559
},
560560
},
561561
{
@@ -1492,8 +1492,15 @@ class MCPToolServer {
14921492
async _deleteFile(filePath) {
14931493
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(this.projectPath || '', filePath);
14941494
try {
1495-
await fs.unlink(fullPath);
1496-
return { success: true, path: fullPath, message: `File deleted: ${fullPath}` };
1495+
const stats = await fs.stat(fullPath);
1496+
if (stats.isDirectory()) {
1497+
// Recursively delete directory
1498+
await fs.rm(fullPath, { recursive: true, force: true });
1499+
return { success: true, path: fullPath, message: `Directory deleted: ${fullPath}` };
1500+
} else {
1501+
await fs.unlink(fullPath);
1502+
return { success: true, path: fullPath, message: `File deleted: ${fullPath}` };
1503+
}
14971504
} catch (error) {
14981505
return { success: false, error: error.message };
14991506
}
@@ -1586,13 +1593,44 @@ class MCPToolServer {
15861593
}
15871594

15881595
let workDir = this.projectPath || process.cwd();
1589-
if (cwd && path.isAbsolute(cwd)) {
1590-
const cwdNorm = cwd.replace(/\\/g, '/').toLowerCase();
1591-
const projNorm = (this.projectPath || '').replace(/\\/g, '/').toLowerCase();
1592-
if (projNorm && cwdNorm.startsWith(projNorm)) {
1593-
workDir = cwd;
1596+
if (cwd) {
1597+
// Validate cwd parameter
1598+
const cwdStr = String(cwd).trim();
1599+
1600+
// Block obvious invalid values (wildcards, single chars, empty after trim)
1601+
if (!cwdStr || cwdStr === '*' || cwdStr === '?' || /^[*?]+$/.test(cwdStr)) {
1602+
console.log(`[MCPToolServer] Blocked invalid cwd "${cwd}"`);
1603+
return { success: false, error: `Invalid cwd parameter: "${cwd}". Use a valid directory path or omit cwd to use project root.` };
1604+
}
1605+
1606+
if (path.isAbsolute(cwdStr)) {
1607+
const cwdNorm = cwdStr.replace(/\\/g, '/').toLowerCase();
1608+
const projNorm = (this.projectPath || '').replace(/\\/g, '/').toLowerCase();
1609+
if (projNorm && cwdNorm.startsWith(projNorm)) {
1610+
workDir = cwdStr;
1611+
} else {
1612+
console.log(`[MCPToolServer] Ignoring hallucinated cwd "${cwd}", using project path`);
1613+
}
15941614
} else {
1595-
console.log(`[MCPToolServer] Ignoring hallucinated cwd "${cwd}", using project path`);
1615+
// Relative path — resolve relative to project
1616+
const resolved = path.resolve(this.projectPath || process.cwd(), cwdStr);
1617+
const resolvedNorm = resolved.replace(/\\/g, '/').toLowerCase();
1618+
const projNorm = (this.projectPath || '').replace(/\\/g, '/').toLowerCase();
1619+
if (projNorm && resolvedNorm.startsWith(projNorm)) {
1620+
// Check if directory exists
1621+
try {
1622+
const stats = fsSync.statSync(resolved);
1623+
if (stats.isDirectory()) {
1624+
workDir = resolved;
1625+
} else {
1626+
console.log(`[MCPToolServer] cwd "${cwd}" is not a directory, using project path`);
1627+
}
1628+
} catch (e) {
1629+
console.log(`[MCPToolServer] cwd "${cwd}" does not exist, using project path`);
1630+
}
1631+
} else {
1632+
console.log(`[MCPToolServer] Ignoring cwd "${cwd}" — resolves outside project`);
1633+
}
15961634
}
15971635
}
15981636
const timeoutMs = Math.min(Math.max(timeout || 60000, 5000), 300000);

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.8.30",
3+
"version": "1.8.31",
44
"description": "guIDE - AI-Powered Offline IDE with local LLM, RAG, MCP tools, browser automation, and integrated terminal",
55
"author": {
66
"name": "Brendan Gray",

website/src/app/download/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
// Single source of truth for the displayed release version.
44
// Updated automatically by: npm run release:deploy (from IDE root)
5-
const CURRENT_VERSION = '1.8.28';
5+
const CURRENT_VERSION = '1.8.31';
66

77
import Link from 'next/link';
88
import { useState } from 'react';

0 commit comments

Comments
 (0)