aboutsummaryrefslogtreecommitdiff
path: root/modules/core/core-bundle.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/core-bundle.js')
-rw-r--r--modules/core/core-bundle.js65
1 files changed, 57 insertions, 8 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 3f7e352..4a2416c 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -8112,7 +8112,7 @@ class ServerMonitorModule {
// Only deterministic reads are cached — never status/log/polling tools.
this._cacheableTools = new Set(['read_file', 'list_files', 'list_modules', 'view_module',
'nvfs_resolve', 'get_help', 'list_skills', 'diff_module', 'get_module_status']);
- this.maxContextTokens = 2000;
+ this.maxContextTokens = 12000;
this.MAX_TOOL_TURNS = 8;
this._pendingConfirmation = null;
this._confirmResolve = null;
@@ -8217,6 +8217,12 @@ buildSystemPrompt() {
base += '[/Auto-Fix Suggestions]';
}
} catch (e) {}
+ base += '\n\n[File reads, tools, verification]\n' +
+ '- read_file returns at most 8000 chars. If the reply says truncated=true, call read_file again with offset=<next_offset> before concluding anything about the file.\n' +
+ '- Prefer targeted reads over whole files: run_command with grep -n "<pattern>" <file>, sed -n \'10,40p\' <file>, head -40 <file>, tail -40 <file>, wc -l <file>.\n' +
+ '- sed without -i is read-only and allowed; sed -i, rm, mv and writes need user confirmation.\n' +
+ '- After ANY write or edit: re-read the changed lines, then run a check (test, node scripts/lint-no-undef.py, or a curl) and say in one line what you verified.\n' +
+ '- If a tool fails twice, change approach (different tool, smaller scope) instead of retrying the same call.';
return base;
}
getToolDefinitions() {
@@ -8249,8 +8255,12 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'read_file',
- description: 'Read VFS file',
- parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path' } }, required: ['path'] }
+ description: 'Read a VFS file. Large files come back in pages: if the reply has truncated=true, call again with offset=<next_offset> instead of guessing or giving up. Returns JSON {path, offset, returned, total_chars, truncated, next_offset, content}.',
+ parameters: { type: 'object', properties: {
+ path: { type: 'string', description: 'File path' },
+ offset: { type: 'integer', description: 'Start character offset (default 0)' },
+ limit: { type: 'integer', description: 'Max characters to return (default 8000, max 32000)' }
+ }, required: ['path'] }
}
}, {
type: 'function',
@@ -9001,6 +9011,12 @@ buildSystemPrompt() {
if (loopResult.finalAnswer) {
sess.context.push({ role: 'assistant', content: loopResult.finalAnswer });
}
+ // Durable trace of what was read/run this turn (2026-10-21) — without this,
+ // every later turn started blind and the model re-read the same files.
+ if (this._turnObservations && this._turnObservations.length) {
+ sess.context.push({ role: 'assistant', content: '⚙️ tool trace: ' + this._turnObservations.join(' | ') });
+ }
+ this._turnObservations = [];
if (sess.context.length > this.maxContext * 2) {
sess.context.splice(0, sess.context.length - this.maxContext * 2);
}
@@ -9363,6 +9379,19 @@ buildSystemPrompt() {
}
return result.substring(0, maxLen) + '...(truncated from ' + result.length + ' chars)';
}
+ // 2026-10-21: tool results lived only inside the current turn, so the next turn
+ // had no idea what the model had already read or run. Keep a compact per-turn
+ // trace and persist it into the session context (see the turn-end push).
+ _noteObservation(name, args, result) {
+ try {
+ if (!this._turnObservations) this._turnObservations = [];
+ if (this._turnObservations.length >= 12) return;
+ const argStr = (typeof args === 'object' && args !== null) ? JSON.stringify(args) : String(args || '');
+ const note = (name + '(' + argStr + ') → ' + String(result === undefined || result === null ? '' : result))
+ .replace(/\s+/g, ' ').trim();
+ this._turnObservations.push(note.slice(0, 240));
+ } catch (_e) {}
+ }
_escapeHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
@@ -9710,6 +9739,7 @@ buildSystemPrompt() {
}
} catch (_e) {}
messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResult });
+ this._noteObservation(fn.name, fn.arguments, toolResult);
}
// Only block on destructive tools
if (destructiveTools.length > 0) {
@@ -9784,6 +9814,7 @@ buildSystemPrompt() {
}
} catch (_e) {}
messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResult });
+ this._noteObservation(fn.name, fn.arguments, toolResult);
}
} else {
messages.push({
@@ -10400,8 +10431,20 @@ buildSystemPrompt() {
const path = rp.replace(/^\//, '');
const content = this.terminal.fs.readFile(path);
this.terminal.fs.currentPath = savedR;
- if (content === null) return JSON.stringify({ error: `File not found: ${path}` });
- return content.length > 8000 ? content.substring(0, 8000) + '\n...(truncated)' : content;
+ if (content === null || content === undefined) return JSON.stringify({ error: `File not found: ${path}` });
+ // Paged reads (2026-10-21): the old 8000-char hard cap made any
+ // bigger file unreadable — the model could neither finish it nor
+ // resume. Now the caller pages with offset/limit and gets next_offset.
+ const total = content.length;
+ const offset = Math.max(0, Math.min(parseInt(a.offset, 10) || 0, total));
+ const limit = Math.max(1000, Math.min(parseInt(a.limit, 10) || 8000, 32000));
+ const slice = content.slice(offset, offset + limit);
+ const nextOffset = offset + slice.length < total ? offset + slice.length : null;
+ return JSON.stringify({
+ path: path, offset: offset, returned: slice.length,
+ total_chars: total, truncated: nextOffset !== null,
+ next_offset: nextOffset, content: slice
+ });
}
case 'write_file': {
const wp = a.path || '';
@@ -11956,18 +11999,24 @@ buildSystemPrompt() {
if (this.context.length <= 6) return;
const totalTokens = this._estimateContextTokens();
if (totalTokens < this.maxContextTokens) return;
- const keepCount = 3;
+ const keepCount = 8;
const oldMsgs = this.context.slice(0, -keepCount * 2);
if (oldMsgs.length < 2) return;
const recentMsgs = this.context.slice(-keepCount * 2);
+ // Tool traces are the only durable record of what was read/run — keep them
+ // verbatim instead of folding them into the 400-char digest (2026-10-21).
const summaryText = oldMsgs.map(m =>
+ /^⚙️ tool trace:/.test(m.content || '') ? (m.content || '').slice(0, 600) :
(m.role === 'user' ? '👤' : m.role === 'assistant' ? '🤖' : '⚙️') + ': ' +
- (m.content || '').substring(0, 150)
+ (m.content || '').substring(0, 400)
).join(' | ');
let compressed = `[Earlier context (${oldMsgs.length} messages): ${summaryText}]`;
+ // Tool traces must survive compression, so the AI one-sentence summary is only
+ // used when there are none to lose (2026-10-21).
+ const _traces = oldMsgs.filter(m => /^⚙️ tool trace:/.test(m.content || ''));
// Try AI summarization, fallback to truncation
try {
- if (this.terminal.ollamaManager && this.terminal.ollamaManager.connected) {
+ if (!_traces.length && this.terminal.ollamaManager && this.terminal.ollamaManager.connected) {
const sumPrompt = 'Summarize these conversation exchanges into a single compact sentence. Focus on what the user asked and what actions were taken. Keep it under 200 characters.\n\n' + JSON.stringify(oldMsgs.map(m => ({ role: m.role, content: (m.content || '').substring(0, 200) })));
const sumRes = await this.terminal.ollamaManager.chat(sumPrompt);
if (sumRes && sumRes.length < 300) {