diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-27 17:08:42 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-27 17:08:42 +0100 |
| commit | d2588d521b304d0141cfba1f5c8dd4ec50492097 (patch) | |
| tree | c88017dd6d716d4ab04dd1494e418cd09ec47c37 /modules/core | |
| parent | b7b0c3481760315ba49f5db1a7e61079b316f8af (diff) | |
fix(newos4): send num_ctx to Ollama, and make one auto-connect failure read as one line
Two things I had flagged as "worth your attention" and one leftover nit.
- num_ctx was never sent. Ollama's server-side default context is 2k-4k depending on the build, so
a system prompt + 20 messages + up to 24k chars of tool results could be truncated server-side,
which is exactly the "it forgot what it just read" class of report. Every Ollama request now
carries options.num_ctx (default 16384, overridable via webx11_ollama_numctx, floor 512); the
OpenAI-compatible branch is untouched because num_ctx is not one of its parameters.
- Automatic connect prints one outcome instead of one line per endpoint tried (7 -> 3 on a dead
endpoint). Interactive `ollamaconnect` keeps the per-endpoint detail.
- write_file's failure message no longer echoes a trailing slash.
Verification:
- test-ollama-numctx.js (7 asserts): generate and chat payloads carry num_ctx=16384, a stored
override is used, junk/too-small values fall back, and the /v1/chat/completions branch does not
receive it. RED on HEAD: TypeError reading 'num_ctx' of undefined. Suite 20/20.
- Live v4.6.117, capturing the real request: {"num_ctx":16384} on /api/generate with
model gemma4:31b-cloud, reply "OK".
- Live v4.6.118, dead endpoint (127.0.0.1:9, fallback disabled): automatic -> 3 lines
("connecting on first use…", "Connecting to dead…", "Auto-connect failed — …"); manual
connect() -> 4 lines including "endpoint http://127.0.0.1:9 failed", so the suppression is
conditional, not blanket.
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 38 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 2 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/ollama-manager.js | 36 |
4 files changed, 57 insertions, 21 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 1a92ef9..fcf1505 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -1290,6 +1290,19 @@ class OllamaManager { // ─── Provider management ───────────────────────────────────────────────── + // Ollama's server-side default context is small (2k-4k depending on the build). The agent + // sends a system prompt, up to 20 messages and up to 24k chars of tool results, so without an + // explicit num_ctx the server silently truncates the prompt — the model "forgets" what it was + // just told. Send it on every Ollama request; the OpenAI-compatible branch has its own params. + _ctxTokens() { + const n = Number(this.numCtx || (typeof localStorage !== 'undefined' ? localStorage.getItem('webx11_ollama_numctx') : 0)); + return Number.isFinite(n) && n >= 512 ? Math.floor(n) : 16384; + } + + _ollamaOptions(extra) { + return Object.assign({ num_ctx: this._ctxTokens() }, extra || {}); + } + _loadProviders() { const stored = this._getSecret('webx11_llm_providers'); if (stored) { @@ -1506,7 +1519,7 @@ class OllamaManager { break; } catch (error) { this.terminal.log('WARN', 'Ollama', provider.name + ' endpoint ' + ep + ' failed: ' + error.message); - this.terminal.print(`${provider.name} endpoint ${ep} failed: ${error.message}`, 'warning'); + if (!this._quietConnect) this.terminal.print(`${provider.name} endpoint ${ep} failed: ${error.message}`, 'warning'); } } } else { @@ -1544,7 +1557,7 @@ class OllamaManager { success = true; } catch (error) { this.terminal.log('WARN', 'Ollama', provider.name + ' failed: ' + error.message); - this.terminal.print(`${provider.name} failed: ${error.message}`, 'warning'); + if (!this._quietConnect) this.terminal.print(`${provider.name} failed: ${error.message}`, 'warning'); } } @@ -1554,7 +1567,7 @@ class OllamaManager { // If provider-based connect failed, try bare endpoint fallback (legacy) for (const ep of allEndpoints) { try { - this.terminal.print(`Connecting (legacy fallback) to ${ep}...`, 'info'); + if (!this._quietConnect) this.terminal.print(`Connecting (legacy fallback) to ${ep}...`, 'info'); const response = await fetch(`${ep}/api/tags`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); @@ -1580,7 +1593,7 @@ class OllamaManager { return true; } catch (error) { this.terminal.log('WARN', 'Ollama', 'Endpoint ' + ep + ' failed: ' + error.message); - this.terminal.print(`Endpoint ${ep} failed: ${error.message}`, 'warning'); + if (!this._quietConnect) this.terminal.print(`Endpoint ${ep} failed: ${error.message}`, 'warning'); } } @@ -1622,6 +1635,10 @@ class OllamaManager { this.terminal.print('Ollama: connecting on first use…', 'info'); let timedOut = false; try { + // One failure should read as one failure: connect() prints a line per endpoint it + // tries, which made an auto-connect failure seven lines of noise. Keep the detail in + // the log, print only the outcome for an automatic attempt. + this._quietConnect = true; this._connecting = this.connect(); // connect() uses a bare fetch(), so a blackholed endpoint would hang this promise // forever and — because the promise is shared — every later message with it. @@ -1635,6 +1652,7 @@ class OllamaManager { // A throwing terminal.log() must not leave _connecting poisoned: log defensively. try { this.terminal.log('WARN', 'Ollama', 'Auto-connect failed: ' + (e && e.message ? e.message : e)); } catch (_) {} } finally { + this._quietConnect = false; this._connecting = null; } if (this.connected && savedModel && !(this.models || []).some(m => m.name === savedModel)) { @@ -1706,7 +1724,7 @@ class OllamaManager { const response = await fetch(`${provider.endpoint}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled }), + body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled, options: this._ollamaOptions() }), signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -1876,7 +1894,7 @@ class OllamaManager { const response = await fetch(`${provider.endpoint}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled }), + body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled, options: this._ollamaOptions() }), signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -2053,7 +2071,7 @@ class OllamaManager { try { if (provider.type === 'ollama') { - const body = { model: useModel, messages, stream: false }; + const body = { model: useModel, messages, stream: false, options: this._ollamaOptions() }; if (tools && tools.length > 0) body.tools = tools; const t0 = Date.now(); const response = await fetch(`${provider.endpoint}/api/chat`, { @@ -2286,7 +2304,7 @@ ${toolDesc} Only output XML function calls for tool tasks. If no tool is needed, answer normally.` }; const msgsWithTools = [toolContext, ...messages]; - const body = { model: useModel, messages: msgsWithTools, stream: false, options: { temperature: 0.1 } }; + const body = { model: useModel, messages: msgsWithTools, stream: false, options: this._ollamaOptions({ temperature: 0.1 }) }; const response = await fetch(`${this.endpoint}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -2337,7 +2355,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm // Original Ollama fallback this.terminal.print(`Model doesn't support tool calling. Retrying /api/chat without tools...`, 'warning'); try { - const body = { model: useModel, messages, stream: false }; + const body = { model: useModel, messages, stream: false, options: this._ollamaOptions() }; const response = await fetch(`${this.endpoint}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), @@ -11084,7 +11102,7 @@ buildSystemPrompt() { const why = (typeof this.terminal.fs._isVirtualPath === 'function' && this.terminal.fs._isVirtualPath(wp)) ? 'read-only virtual path' : 'parent directory missing or target is a directory'; - return `Write FAILED: ${wp} — ${why}; nothing was written`; + return `Write FAILED: ${String(wp).replace(/\/+$/, '') || '/'} — ${why}; nothing was written`; } return `File written: ${wp} (${content.length} chars)`; } diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 26e47cb..e4b7b7e 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -2509,7 +2509,7 @@ buildSystemPrompt() { const why = (typeof this.terminal.fs._isVirtualPath === 'function' && this.terminal.fs._isVirtualPath(wp)) ? 'read-only virtual path' : 'parent directory missing or target is a directory'; - return `Write FAILED: ${wp} — ${why}; nothing was written`; + return `Write FAILED: ${String(wp).replace(/\/+$/, '') || '/'} — ${why}; nothing was written`; } return `File written: ${wp} (${content.length} chars)`; } diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index b74987e..c383f23 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.116'; + const LATEST_VER = 'v4.6.118'; const CACHE = '?v=' + LATEST_VER; // Exposed so ext modules can register their own URL with the same cache-busting version // instead of an unversioned path the browser may already have cached (shell.js did). diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js index 4512662..7dcecc3 100644 --- a/modules/core/ollama-manager.js +++ b/modules/core/ollama-manager.js @@ -55,6 +55,19 @@ class OllamaManager { // ─── Provider management ───────────────────────────────────────────────── + // Ollama's server-side default context is small (2k-4k depending on the build). The agent + // sends a system prompt, up to 20 messages and up to 24k chars of tool results, so without an + // explicit num_ctx the server silently truncates the prompt — the model "forgets" what it was + // just told. Send it on every Ollama request; the OpenAI-compatible branch has its own params. + _ctxTokens() { + const n = Number(this.numCtx || (typeof localStorage !== 'undefined' ? localStorage.getItem('webx11_ollama_numctx') : 0)); + return Number.isFinite(n) && n >= 512 ? Math.floor(n) : 16384; + } + + _ollamaOptions(extra) { + return Object.assign({ num_ctx: this._ctxTokens() }, extra || {}); + } + _loadProviders() { const stored = this._getSecret('webx11_llm_providers'); if (stored) { @@ -271,7 +284,7 @@ class OllamaManager { break; } catch (error) { this.terminal.log('WARN', 'Ollama', provider.name + ' endpoint ' + ep + ' failed: ' + error.message); - this.terminal.print(`${provider.name} endpoint ${ep} failed: ${error.message}`, 'warning'); + if (!this._quietConnect) this.terminal.print(`${provider.name} endpoint ${ep} failed: ${error.message}`, 'warning'); } } } else { @@ -309,7 +322,7 @@ class OllamaManager { success = true; } catch (error) { this.terminal.log('WARN', 'Ollama', provider.name + ' failed: ' + error.message); - this.terminal.print(`${provider.name} failed: ${error.message}`, 'warning'); + if (!this._quietConnect) this.terminal.print(`${provider.name} failed: ${error.message}`, 'warning'); } } @@ -319,7 +332,7 @@ class OllamaManager { // If provider-based connect failed, try bare endpoint fallback (legacy) for (const ep of allEndpoints) { try { - this.terminal.print(`Connecting (legacy fallback) to ${ep}...`, 'info'); + if (!this._quietConnect) this.terminal.print(`Connecting (legacy fallback) to ${ep}...`, 'info'); const response = await fetch(`${ep}/api/tags`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); @@ -345,7 +358,7 @@ class OllamaManager { return true; } catch (error) { this.terminal.log('WARN', 'Ollama', 'Endpoint ' + ep + ' failed: ' + error.message); - this.terminal.print(`Endpoint ${ep} failed: ${error.message}`, 'warning'); + if (!this._quietConnect) this.terminal.print(`Endpoint ${ep} failed: ${error.message}`, 'warning'); } } @@ -387,6 +400,10 @@ class OllamaManager { this.terminal.print('Ollama: connecting on first use…', 'info'); let timedOut = false; try { + // One failure should read as one failure: connect() prints a line per endpoint it + // tries, which made an auto-connect failure seven lines of noise. Keep the detail in + // the log, print only the outcome for an automatic attempt. + this._quietConnect = true; this._connecting = this.connect(); // connect() uses a bare fetch(), so a blackholed endpoint would hang this promise // forever and — because the promise is shared — every later message with it. @@ -400,6 +417,7 @@ class OllamaManager { // A throwing terminal.log() must not leave _connecting poisoned: log defensively. try { this.terminal.log('WARN', 'Ollama', 'Auto-connect failed: ' + (e && e.message ? e.message : e)); } catch (_) {} } finally { + this._quietConnect = false; this._connecting = null; } if (this.connected && savedModel && !(this.models || []).some(m => m.name === savedModel)) { @@ -471,7 +489,7 @@ class OllamaManager { const response = await fetch(`${provider.endpoint}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled }), + body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled, options: this._ollamaOptions() }), signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -641,7 +659,7 @@ class OllamaManager { const response = await fetch(`${provider.endpoint}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled }), + body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled, options: this._ollamaOptions() }), signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -818,7 +836,7 @@ class OllamaManager { try { if (provider.type === 'ollama') { - const body = { model: useModel, messages, stream: false }; + const body = { model: useModel, messages, stream: false, options: this._ollamaOptions() }; if (tools && tools.length > 0) body.tools = tools; const t0 = Date.now(); const response = await fetch(`${provider.endpoint}/api/chat`, { @@ -1051,7 +1069,7 @@ ${toolDesc} Only output XML function calls for tool tasks. If no tool is needed, answer normally.` }; const msgsWithTools = [toolContext, ...messages]; - const body = { model: useModel, messages: msgsWithTools, stream: false, options: { temperature: 0.1 } }; + const body = { model: useModel, messages: msgsWithTools, stream: false, options: this._ollamaOptions({ temperature: 0.1 }) }; const response = await fetch(`${this.endpoint}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -1102,7 +1120,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm // Original Ollama fallback this.terminal.print(`Model doesn't support tool calling. Retrying /api/chat without tools...`, 'warning'); try { - const body = { model: useModel, messages, stream: false }; + const body = { model: useModel, messages, stream: false, options: this._ollamaOptions() }; const response = await fetch(`${this.endpoint}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), |