diff options
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 69 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 44 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-memory-graph.js | 25 |
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; |