From 5b05af91fa3fcc9ae7aff00feedc26d062babbc2 Mon Sep 17 00:00:00 2001 From: Agando Date: Fri, 23 Oct 2026 21:43:45 +0200 Subject: feat(newos4): context window warns at 80% and says what trimming dropped (v4.6.91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0-5 asked what to do about a 422k-token session against a 400k threshold. There is no 400k threshold in this tree — the budgets are maxContextTokens (12000) and maxContext*2 messages — so rather than chase a number that does not exist, this applies the decision that is defensible either way: warn before the cliff, never lose history without saying so. - 80% of the token budget prints one warning per crossing (no per-turn nagging) - the message-count trim unshifts a marker naming how many messages left the window and quoting the earliest one, instead of splicing them away silently - 'max-context-tokens (default: 2000)' now reports the real code default, 12000 Verified: 900/1000 tokens -> warns once, second call silent; 1500/1000 -> compresses to 17 messages with the summary head intact; 9-message window with maxContext=2 -> 4 messages, marker '[context trim: 6 earlier message(s) ... earliest was: "turn 0 ..."]'. --- modules/core/core-bundle.js | 29 ++++++++++++++++++++++++++--- modules/core/module-aiassist.js | 29 ++++++++++++++++++++++++++--- modules/core/module-loader.js | 2 +- 3 files changed, 53 insertions(+), 7 deletions(-) (limited to 'modules/core') diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index f72a20e..8e19189 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -9348,7 +9348,17 @@ buildSystemPrompt() { // every later turn started blind and the model re-read the same files. this._flushToolTrace(sess, _traceTag); if (sess.context.length > this.maxContext * 2) { - sess.context.splice(0, sess.context.length - this.maxContext * 2); + // Silently splicing the opening turns is how a long task forgets its own brief: + // the model then "remembers" a conversation that never started that way. Drop + // the same messages, but leave a marker saying how many and what the first was. + const cap = this.maxContext * 2; + const dropped = sess.context.splice(0, sess.context.length - (cap - 1)); + const firstUser = dropped.find(m => m.role === 'user'); + const note = '[context trim: ' + dropped.length + ' earlier message(s) left this window' + + (firstUser && firstUser.content ? ' — the earliest was: "' + String(firstUser.content).replace(/\s+/g, ' ').slice(0, 90) + '…"' : '') + + ']'; + sess.context.unshift({ role: 'system', content: note }); + this.terminal.print('✂️ Context trimmed: ' + dropped.length + ' message(s) — the model is told what it lost', 'text-muted'); } // Reflection: after complex tasks, run health scan and auto-skill creation if (loopResult.toolTurns >= 2 && !this._cancelled) { @@ -12197,7 +12207,7 @@ buildSystemPrompt() { this.terminal.print('=== AI Config ===', 'info'); this.terminal.print(`max-turns: ${this.MAX_TOOL_TURNS} (default: 8)`, ''); this.terminal.print(`max-context: ${this.maxContext} (default: 10)`, ''); - this.terminal.print(`max-context-tokens: ${this.maxContextTokens} (default: 2000)`, ''); + this.terminal.print(`max-context-tokens: ${this.maxContextTokens} (code default: 12000)`, ''); this.terminal.print(`trivial-optimization: ${(this.terminal?.ollamaManager?.optimizationEnabled) ? 'on' : 'off (default)'}`, ''); this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, ''); this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, ''); @@ -12520,7 +12530,20 @@ buildSystemPrompt() { async _compressContext() { if (this.context.length <= 6) return; const totalTokens = this._estimateContextTokens(); - if (totalTokens < this.maxContextTokens) return; + if (totalTokens < this.maxContextTokens) { + // Warn before the cliff rather than only reporting the fall. Once per crossing, + // so a long session is not nagged every turn. + const pct = Math.round((totalTokens / this.maxContextTokens) * 100); + if (pct >= 80 && !this._ctxWarned) { + this._ctxWarned = true; + this.terminal.print('⚠️ Context ' + pct + '% full (' + totalTokens + '/' + this.maxContextTokens + + ' tokens) — the next few turns will compress earlier history into a summary', 'warning'); + } else if (pct < 60) { + this._ctxWarned = false; + } + return; + } + this._ctxWarned = false; const keepCount = 8; const oldMsgs = this.context.slice(0, -keepCount * 2); if (oldMsgs.length < 2) return; diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 55dde6f..43cbb11 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -950,7 +950,17 @@ buildSystemPrompt() { // every later turn started blind and the model re-read the same files. this._flushToolTrace(sess, _traceTag); if (sess.context.length > this.maxContext * 2) { - sess.context.splice(0, sess.context.length - this.maxContext * 2); + // Silently splicing the opening turns is how a long task forgets its own brief: + // the model then "remembers" a conversation that never started that way. Drop + // the same messages, but leave a marker saying how many and what the first was. + const cap = this.maxContext * 2; + const dropped = sess.context.splice(0, sess.context.length - (cap - 1)); + const firstUser = dropped.find(m => m.role === 'user'); + const note = '[context trim: ' + dropped.length + ' earlier message(s) left this window' + + (firstUser && firstUser.content ? ' — the earliest was: "' + String(firstUser.content).replace(/\s+/g, ' ').slice(0, 90) + '…"' : '') + + ']'; + sess.context.unshift({ role: 'system', content: note }); + this.terminal.print('✂️ Context trimmed: ' + dropped.length + ' message(s) — the model is told what it lost', 'text-muted'); } // Reflection: after complex tasks, run health scan and auto-skill creation if (loopResult.toolTurns >= 2 && !this._cancelled) { @@ -3799,7 +3809,7 @@ buildSystemPrompt() { this.terminal.print('=== AI Config ===', 'info'); this.terminal.print(`max-turns: ${this.MAX_TOOL_TURNS} (default: 8)`, ''); this.terminal.print(`max-context: ${this.maxContext} (default: 10)`, ''); - this.terminal.print(`max-context-tokens: ${this.maxContextTokens} (default: 2000)`, ''); + this.terminal.print(`max-context-tokens: ${this.maxContextTokens} (code default: 12000)`, ''); this.terminal.print(`trivial-optimization: ${(this.terminal?.ollamaManager?.optimizationEnabled) ? 'on' : 'off (default)'}`, ''); this.terminal.print(`confirm-timeout: ${this._confirmTimeoutMs > 0 ? this._confirmTimeoutMs / 1000 + 's' : 'disabled (no auto-deny)'} (default: 120s)`, ''); this.terminal.print(`chat-lock-timeout: ${this._chatLockTimeoutMs / 1000}s (default: 10s)`, ''); @@ -4122,7 +4132,20 @@ buildSystemPrompt() { async _compressContext() { if (this.context.length <= 6) return; const totalTokens = this._estimateContextTokens(); - if (totalTokens < this.maxContextTokens) return; + if (totalTokens < this.maxContextTokens) { + // Warn before the cliff rather than only reporting the fall. Once per crossing, + // so a long session is not nagged every turn. + const pct = Math.round((totalTokens / this.maxContextTokens) * 100); + if (pct >= 80 && !this._ctxWarned) { + this._ctxWarned = true; + this.terminal.print('⚠️ Context ' + pct + '% full (' + totalTokens + '/' + this.maxContextTokens + + ' tokens) — the next few turns will compress earlier history into a summary', 'warning'); + } else if (pct < 60) { + this._ctxWarned = false; + } + return; + } + this._ctxWarned = false; const keepCount = 8; const oldMsgs = this.context.slice(0, -keepCount * 2); if (oldMsgs.length < 2) return; diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 95176d9..3ab51d7 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.90'; + const LATEST_VER = 'v4.6.91'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { -- cgit v1.2.3