diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-27 16:07:35 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-27 16:07:35 +0100 |
| commit | 2e9401a5587b0a45daaf0af116bb7da573b0988d (patch) | |
| tree | 147fd3f1db8d00982de248345c86c66b2c23bfbb /modules/core | |
| parent | c1792f0b09990af95eed2ccfbe3d365902147547 (diff) | |
fix(newos4): the loop guard reads the config it documents
MAINT-004. The guard read warnAt/blockAt/hardStopAt while one config path wrote `threshold`, and
`enabled` was never consulted anywhere — writing {"enabled": false} into webx11_ai_config changed
nothing at all. One resolver, _loopPolicy(), now normalises both spellings, guarantees
warnAt <= blockAt < hardStopAt, and both the block and the warning are gated on enabled.
Live on v4.6.110 (_loopPolicy called in the page): defaults {warn 3, block 6, hardstop 9};
{threshold: 4} -> {warn 2, block 4, hardstop 7}; {enabled: false, threshold: 3} -> enabled false.
test-loop-policy.js: 17 asserts, RED on HEAD ("_loopPolicy is not a function"), suite 16/16.
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 30 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 30 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 |
3 files changed, 49 insertions, 13 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 6e9542e..8d6cbda 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -9841,6 +9841,23 @@ buildSystemPrompt() { } return result.substring(0, maxLen) + '...(truncated from ' + result.length + ' chars)'; } + // MAINT-004: the loop guard read warnAt/blockAt/hardStopAt while one config path wrote + // `threshold`, and `enabled` was never consulted at all — turning the guard off in + // config changed nothing. One resolver, one set of defaults, explicit disable. + _loopPolicy(cfg) { + const c = cfg || {}; + const threshold = Number(c.threshold); + const blockAt = Number(c.blockAt) || (Number.isFinite(threshold) && threshold > 0 ? threshold : 6); + const warnAt = Number(c.warnAt) || Math.max(2, blockAt - 3); + const hardStopAt = Number(c.hardStopAt) || blockAt + 3; + return { + enabled: c.enabled !== false, + windowMs: Number(c.windowMs) || 30000, + warnAt: Math.min(warnAt, blockAt), + blockAt, + hardStopAt: Math.max(hardStopAt, blockAt + 1) + }; + } // 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 @@ -10043,6 +10060,7 @@ buildSystemPrompt() { } catch (_e) {} }; var loopDetect = loopCfg.loopDetection || { enabled: true, threshold: 6, windowMs: 30000 }; + var _lp = this._loopPolicy(loopDetect); var confEsc = loopCfg.confidenceEscalation || { enabled: true, threshold: 90 }; var finalPrompt = loopCfg.finalTurnPrompt || 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.'; // Memory-augmented planning: inject prior loop lessons as negative constraints @@ -10077,10 +10095,10 @@ buildSystemPrompt() { } catch (e) { /* VFS not available — skip memory-augmented planning */ } let toolTurns = 0; const _seenCalls = new Map(); // sig -> {count, ts, resultHash, blocked, lastResult} - const _loopWindow = loopDetect.windowMs || 30000; - const _warnAt = loopDetect.warnAt || 3; - const _blockAt = loopDetect.blockAt || 6; - const _hardStopAt = loopDetect.hardStopAt || 9; + const _loopWindow = _lp.windowMs; + const _warnAt = _lp.warnAt; + const _blockAt = _lp.blockAt; + const _hardStopAt = _lp.hardStopAt; const _agentTag = opts.agentTag || 'main'; this._pendingBlockedReplays = null; const _sigHash = (s) => { @@ -10166,13 +10184,13 @@ buildSystemPrompt() { const argPreview = String(_argKey || '') .replace(/(token|key|secret|password|bearer)[^,}]*/ig, '$1:[redacted]') .slice(0, 60); - if (entry.blocked || entry.count >= _blockAt) { + if (_lp.enabled && (entry.blocked || entry.count >= _blockAt)) { entry.blocked = true; blocked.push({ tc, entry }); this.terminal.print(`🔄 Loop guard: ${_agentTag} repeated ${tc.function.name}(${argPreview}) ${entry.count}x with an unchanged result — serving cached result, not re-running`, 'warning'); continue; } - if (entry.count === _warnAt) { + if (_lp.enabled && entry.count === _warnAt) { this.terminal.print(`⚠️ Loop warning: ${_agentTag} called ${tc.function.name}(${argPreview}) ${entry.count}x — reuse the previous result or change the request`, 'warning'); } } diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 2ee95e9..26e47cb 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -1322,6 +1322,23 @@ buildSystemPrompt() { } return result.substring(0, maxLen) + '...(truncated from ' + result.length + ' chars)'; } + // MAINT-004: the loop guard read warnAt/blockAt/hardStopAt while one config path wrote + // `threshold`, and `enabled` was never consulted at all — turning the guard off in + // config changed nothing. One resolver, one set of defaults, explicit disable. + _loopPolicy(cfg) { + const c = cfg || {}; + const threshold = Number(c.threshold); + const blockAt = Number(c.blockAt) || (Number.isFinite(threshold) && threshold > 0 ? threshold : 6); + const warnAt = Number(c.warnAt) || Math.max(2, blockAt - 3); + const hardStopAt = Number(c.hardStopAt) || blockAt + 3; + return { + enabled: c.enabled !== false, + windowMs: Number(c.windowMs) || 30000, + warnAt: Math.min(warnAt, blockAt), + blockAt, + hardStopAt: Math.max(hardStopAt, blockAt + 1) + }; + } // 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 @@ -1524,6 +1541,7 @@ buildSystemPrompt() { } catch (_e) {} }; var loopDetect = loopCfg.loopDetection || { enabled: true, threshold: 6, windowMs: 30000 }; + var _lp = this._loopPolicy(loopDetect); var confEsc = loopCfg.confidenceEscalation || { enabled: true, threshold: 90 }; var finalPrompt = loopCfg.finalTurnPrompt || 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.'; // Memory-augmented planning: inject prior loop lessons as negative constraints @@ -1558,10 +1576,10 @@ buildSystemPrompt() { } catch (e) { /* VFS not available — skip memory-augmented planning */ } let toolTurns = 0; const _seenCalls = new Map(); // sig -> {count, ts, resultHash, blocked, lastResult} - const _loopWindow = loopDetect.windowMs || 30000; - const _warnAt = loopDetect.warnAt || 3; - const _blockAt = loopDetect.blockAt || 6; - const _hardStopAt = loopDetect.hardStopAt || 9; + const _loopWindow = _lp.windowMs; + const _warnAt = _lp.warnAt; + const _blockAt = _lp.blockAt; + const _hardStopAt = _lp.hardStopAt; const _agentTag = opts.agentTag || 'main'; this._pendingBlockedReplays = null; const _sigHash = (s) => { @@ -1647,13 +1665,13 @@ buildSystemPrompt() { const argPreview = String(_argKey || '') .replace(/(token|key|secret|password|bearer)[^,}]*/ig, '$1:[redacted]') .slice(0, 60); - if (entry.blocked || entry.count >= _blockAt) { + if (_lp.enabled && (entry.blocked || entry.count >= _blockAt)) { entry.blocked = true; blocked.push({ tc, entry }); this.terminal.print(`🔄 Loop guard: ${_agentTag} repeated ${tc.function.name}(${argPreview}) ${entry.count}x with an unchanged result — serving cached result, not re-running`, 'warning'); continue; } - if (entry.count === _warnAt) { + if (_lp.enabled && entry.count === _warnAt) { this.terminal.print(`⚠️ Loop warning: ${_agentTag} called ${tc.function.name}(${argPreview}) ${entry.count}x — reuse the previous result or change the request`, 'warning'); } } diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index da81184..329d0f7 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.109'; + const LATEST_VER = 'v4.6.110'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { |