diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-23 21:43:45 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-23 21:43:45 +0200 |
| commit | 5b05af91fa3fcc9ae7aff00feedc26d062babbc2 (patch) | |
| tree | 7060f31c71275ad64a22a8ccdab3260ea42d3208 | |
| parent | d5b6e6f12c8c3c4551050f8fc7d19fb78d6b581b (diff) | |
feat(newos4): context window warns at 80% and says what trimming dropped (v4.6.91)
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 ..."]'.
| -rw-r--r-- | help.html | 19 | ||||
| -rw-r--r-- | main.html | 6 | ||||
| -rw-r--r-- | modules/core/core-bundle.js | 29 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 29 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 |
5 files changed, 75 insertions, 10 deletions
@@ -1689,6 +1689,25 @@ listmods</pre> <h2>📋 Changelog</h2> <div class="version-item"> + <div class="v-header"><span class="v-num">v4.6.91 — October 2026</span> + <span class="v-date">context says what it dropped</span></div> + <ul> + <li><strong>The window warns before it forgets.</strong> At 80% of the token budget the + terminal says so once — <code>⚠️ Context 90% full (900/1000 tokens)</code> — instead of + only announcing the fall after compression. It does not repeat on every turn.</li> + <li><strong>Trimming leaves a marker.</strong> Old turns used to vanish silently, so a long + task "remembered" a conversation that never started that way. The window now opens with + <code>[context trim: 6 earlier message(s) left this window — the earliest was: "turn 0 + original brief text…"]</code>.</li> + <li><strong>The help text stopped lying.</strong> <code>max-context-tokens</code> claimed a + default of 2000; the code has always used 12000.</li> + <li><strong>On the "400k / 422k" report:</strong> no such limit exists in this codebase — + the only budgets are <code>maxContextTokens</code> (12 000) and <code>maxContext × 2</code> + messages. That figure came from outside the app; nothing here was changed to chase it.</li> + </ul> + </div> + + <div class="version-item"> <div class="v-header"><span class="v-num">v4.6.88–90 — October 2026</span> <span class="v-date">one phantom entry, three bugs</span></div> <ul> @@ -1349,13 +1349,13 @@ </script> <!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix --> -<script src="modules/core/core-bundle.js?v=v4.6.90"></script> +<script src="modules/core/core-bundle.js?v=v4.6.91"></script> <!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules --> -<script src="modules/core/module-loader.js?v=v4.6.90"></script> +<script src="modules/core/module-loader.js?v=v4.6.91"></script> <!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work --> -<script src="modules/ext/critical-ext-bundle.js?v=v4.6.90"></script> +<script src="modules/ext/critical-ext-bundle.js?v=v4.6.91"></script> <script> // ═══ ONE TRUE VERSION SOURCE ═══ 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) { |