aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/core-bundle.js69
-rw-r--r--modules/core/module-aiassist.js44
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-memory-graph.js25
4 files changed, 135 insertions, 5 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index a057332..c06af5f 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -9837,6 +9837,41 @@ buildSystemPrompt() {
}
return result.substring(0, maxLen) + '...(truncated from ' + result.length + ' chars)';
}
+ // MAINT-023: one agentic turn used to grow without limit — every role:'tool' body was
+ // pushed verbatim and only this.context was trimmed BETWEEN turns, so a measured turn
+ // went 6 → 112 705 tokens across 8 model calls (maxContextTokens is 12 000). Two
+ // limits keep a single turn inside the window: clip each observation as it arrives,
+ // and elide the oldest ones once the running total passes the budget.
+ _clipToolResult(result) {
+ const text = typeof result === 'string' ? result : JSON.stringify(result);
+ const max = this.maxToolResultChars || 6000;
+ if (!text || text.length <= max) return text;
+ return text.slice(0, max)
+ + '\n…[clipped: ' + (text.length - max) + ' of ' + text.length
+ + ' chars withheld — re-read with offset/limit if you need the rest]';
+ }
+ _boundToolContext(messages) {
+ const keepVerbatim = this.toolResultsVerbatim || 4;
+ const budget = this.toolContextBudgetChars || 24000;
+ const idx = [];
+ for (let i = 0; i < messages.length; i++) {
+ if (messages[i] && messages[i].role === 'tool') idx.push(i);
+ }
+ if (!idx.length) return messages;
+ const recent = new Set(idx.slice(-keepVerbatim));
+ let total = 0;
+ for (const i of idx) total += String(messages[i].content || '').length;
+ // Oldest first, never the recent few: those are what the model is working on.
+ for (const i of idx) {
+ if (total <= budget) break;
+ if (recent.has(i)) continue;
+ const len = String(messages[i].content || '').length;
+ const stub = '[earlier tool result elided — ' + len + ' chars; call the tool again if you still need it]';
+ messages[i].content = stub;
+ total -= len - stub.length;
+ }
+ return messages;
+ }
// 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 (per agent tag) and persist it into the session context.
@@ -10067,6 +10102,8 @@ buildSystemPrompt() {
messages.push({ role: 'system', content: finalPrompt });
}
const thinkStart = Date.now();
+ // MAINT-023: keep the in-flight tool context bounded before every model call.
+ this._boundToolContext(messages);
const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions());
const thinkTime = Math.round((Date.now() - thinkStart) / 1000);
// Store raw response for debug-ai-last
@@ -10236,7 +10273,9 @@ buildSystemPrompt() {
_eR.lastResult = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
}
} catch (_e) {}
- messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResult });
+ // MAINT-023: clip on the way in — a single read_file page is 16 000
+ // chars and several of them used to enter the context verbatim.
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: this._clipToolResult(toolResult) });
this._noteObservation(fn.name, fn.arguments, toolResult, _agentTag);
}
// Only block on destructive tools
@@ -10311,7 +10350,8 @@ buildSystemPrompt() {
_eD.lastResult = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
}
} catch (_e) {}
- messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResult });
+ // MAINT-023: clip on the way in (same rule as the main loop).
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: this._clipToolResult(toolResult) });
this._noteObservation(fn.name, fn.arguments, toolResult, _agentTag);
}
} else {
@@ -18163,6 +18203,12 @@ class MemoryGraphModule {
try {
const fs = this.terminal.fs;
if (!fs) return;
+ // MAINT-003: a corrupt store used to look exactly like a fresh one — fsReadJSON
+ // returns null on a parse failure, so the graph started empty with no warning and no
+ // copy of what was there. Quarantine anything unreadable first, then load what's left.
+ this._quarantineCorrupt('export.json');
+ this._quarantineCorrupt('nodes.json');
+ this._quarantineCorrupt('edges.json');
// Try loading from export first
const exportData = fs.fsReadJSON(`${this._indexPath}export.json`);
@@ -18215,6 +18261,25 @@ class MemoryGraphModule {
}
}
+ // MAINT-003: keep an unreadable store aside instead of starting fresh over it.
+ _quarantineCorrupt(name) {
+ try {
+ const fs = this.terminal.fs;
+ const p = `${this._indexPath}${name}`;
+ const raw = fs.readFile(p);
+ if (typeof raw !== 'string' || !raw.length) return false;
+ try { JSON.parse(raw); return false; } catch (e) {
+ const kept = p + '.corrupt';
+ fs.writeFile(kept, raw);
+ this.terminal.print(`⚠️ Memory graph store ${name} is unreadable — kept a copy at ${kept} and starting from what remains`, 'warning');
+ if (typeof fs.deleteFile === 'function') fs.deleteFile(p);
+ return true;
+ }
+ } catch (e) {
+ return false;
+ }
+ }
+
_saveNow() {
try {
const fs = this.terminal.fs;
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index ac8d297..9965e6a 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -1322,6 +1322,41 @@ buildSystemPrompt() {
}
return result.substring(0, maxLen) + '...(truncated from ' + result.length + ' chars)';
}
+ // MAINT-023: one agentic turn used to grow without limit — every role:'tool' body was
+ // pushed verbatim and only this.context was trimmed BETWEEN turns, so a measured turn
+ // went 6 → 112 705 tokens across 8 model calls (maxContextTokens is 12 000). Two
+ // limits keep a single turn inside the window: clip each observation as it arrives,
+ // and elide the oldest ones once the running total passes the budget.
+ _clipToolResult(result) {
+ const text = typeof result === 'string' ? result : JSON.stringify(result);
+ const max = this.maxToolResultChars || 6000;
+ if (!text || text.length <= max) return text;
+ return text.slice(0, max)
+ + '\n…[clipped: ' + (text.length - max) + ' of ' + text.length
+ + ' chars withheld — re-read with offset/limit if you need the rest]';
+ }
+ _boundToolContext(messages) {
+ const keepVerbatim = this.toolResultsVerbatim || 4;
+ const budget = this.toolContextBudgetChars || 24000;
+ const idx = [];
+ for (let i = 0; i < messages.length; i++) {
+ if (messages[i] && messages[i].role === 'tool') idx.push(i);
+ }
+ if (!idx.length) return messages;
+ const recent = new Set(idx.slice(-keepVerbatim));
+ let total = 0;
+ for (const i of idx) total += String(messages[i].content || '').length;
+ // Oldest first, never the recent few: those are what the model is working on.
+ for (const i of idx) {
+ if (total <= budget) break;
+ if (recent.has(i)) continue;
+ const len = String(messages[i].content || '').length;
+ const stub = '[earlier tool result elided — ' + len + ' chars; call the tool again if you still need it]';
+ messages[i].content = stub;
+ total -= len - stub.length;
+ }
+ return messages;
+ }
// 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 (per agent tag) and persist it into the session context.
@@ -1552,6 +1587,8 @@ buildSystemPrompt() {
messages.push({ role: 'system', content: finalPrompt });
}
const thinkStart = Date.now();
+ // MAINT-023: keep the in-flight tool context bounded before every model call.
+ this._boundToolContext(messages);
const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions());
const thinkTime = Math.round((Date.now() - thinkStart) / 1000);
// Store raw response for debug-ai-last
@@ -1721,7 +1758,9 @@ buildSystemPrompt() {
_eR.lastResult = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
}
} catch (_e) {}
- messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResult });
+ // MAINT-023: clip on the way in — a single read_file page is 16 000
+ // chars and several of them used to enter the context verbatim.
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: this._clipToolResult(toolResult) });
this._noteObservation(fn.name, fn.arguments, toolResult, _agentTag);
}
// Only block on destructive tools
@@ -1796,7 +1835,8 @@ buildSystemPrompt() {
_eD.lastResult = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
}
} catch (_e) {}
- messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResult });
+ // MAINT-023: clip on the way in (same rule as the main loop).
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: this._clipToolResult(toolResult) });
this._noteObservation(fn.name, fn.arguments, toolResult, _agentTag);
}
} else {
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 365fa55..3204af0 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = 'v4.6.106';
+ const LATEST_VER = 'v4.6.107';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/module-memory-graph.js b/modules/core/module-memory-graph.js
index 1cb0919..e802387 100644
--- a/modules/core/module-memory-graph.js
+++ b/modules/core/module-memory-graph.js
@@ -594,6 +594,12 @@ class MemoryGraphModule {
try {
const fs = this.terminal.fs;
if (!fs) return;
+ // MAINT-003: a corrupt store used to look exactly like a fresh one — fsReadJSON
+ // returns null on a parse failure, so the graph started empty with no warning and no
+ // copy of what was there. Quarantine anything unreadable first, then load what's left.
+ this._quarantineCorrupt('export.json');
+ this._quarantineCorrupt('nodes.json');
+ this._quarantineCorrupt('edges.json');
// Try loading from export first
const exportData = fs.fsReadJSON(`${this._indexPath}export.json`);
@@ -646,6 +652,25 @@ class MemoryGraphModule {
}
}
+ // MAINT-003: keep an unreadable store aside instead of starting fresh over it.
+ _quarantineCorrupt(name) {
+ try {
+ const fs = this.terminal.fs;
+ const p = `${this._indexPath}${name}`;
+ const raw = fs.readFile(p);
+ if (typeof raw !== 'string' || !raw.length) return false;
+ try { JSON.parse(raw); return false; } catch (e) {
+ const kept = p + '.corrupt';
+ fs.writeFile(kept, raw);
+ this.terminal.print(`⚠️ Memory graph store ${name} is unreadable — kept a copy at ${kept} and starting from what remains`, 'warning');
+ if (typeof fs.deleteFile === 'function') fs.deleteFile(p);
+ return true;
+ }
+ } catch (e) {
+ return false;
+ }
+ }
+
_saveNow() {
try {
const fs = this.terminal.fs;