diff options
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), |