diff options
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 46 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 5 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-world-model.js | 41 |
4 files changed, 73 insertions, 21 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index b565365..0b2795a 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -11557,7 +11557,10 @@ buildSystemPrompt() { facts: wm._answerQuery(a.question || ''), version: wm._model.version, synthesized: wm._model.synthesized, - snapshot: (a.detail === 'full') ? wm._model : JSON.parse(wm._modelDigest()) + // v4.6.83: this was JSON.parse(wm._modelDigest()) and the digest was a + // string truncated mid-token, so the default path always threw + // "Unexpected end of JSON input". The model now hands back an object. + snapshot: (a.detail === 'full') ? wm._model : wm._modelSnapshot() }; return JSON.stringify(wmOut); } @@ -19051,30 +19054,53 @@ class WorldModelModule { // 2026-10-21: ask() used to be a keyword table only, so any real question got // "Unknown query". Whitelist the fields we are willing to send to a model // (never the raw model object: it is persisted alongside other VFS data). - _modelDigest(maxChars) { + // v4.6.83: the digest used to be a JSON string cut at 4000 chars with '…(digest truncated)' + // glued on. Every caller that tried to parse it got "Unexpected end of JSON input" — the + // world_model_query tool did exactly that on its default path. A digest must shrink, never + // break: this returns a valid OBJECT and the string form is only produced at the edge. + _modelSnapshot(maxChars) { const m = this._model; - if (!m) return ''; + if (!m) return null; const cap = maxChars || 4000; - const pick = { + const build = (keep) => ({ version: m.version, synthesized: m.synthesized, system: m.system, modules: m.modules, files: m.files, - skills: { count: m.skills && m.skills.count, skills: ((m.skills && m.skills.skills) || []).slice(0, 40) }, + skills: { count: m.skills && m.skills.count, skills: ((m.skills && m.skills.skills) || []).slice(0, keep) }, memory: m.memory, agents: m.agents, telemetry: m.telemetry, projects: m.projects, - recent: (m.recent || []).slice(0, 20), + recent: (m.recent || []).slice(0, Math.min(keep, 20)), // 2026-10-21: the model only ever exposed a COUNT ("9 recent errors") with no detail, // so neither the user nor the LLM could say what failed. Carry the last few. recentErrorSample: this._errorSample ? this._errorSample() : [], changesSinceLastSnapshot: this._diffSincePrev ? this._diffSincePrev() : [] - }; - let json = JSON.stringify(pick); - if (json.length > cap) json = json.slice(0, cap) + '…(digest truncated)'; - return json; + }); + // Shrink the lists until the snapshot fits. Truncating the JSON text is what broke it. + for (const keep of [40, 20, 10, 5, 3, 1, 0]) { + const pick = build(keep); + if (JSON.stringify(pick).length <= cap) return pick; + } + // Last resort: drop whole sections, in order of least value, until it fits. Still a + // valid object — a caller can always JSON.parse it and see what was left out. + const lean = build(0); + lean.truncated = true; + lean.omitted = []; + for (const key of ['changesSinceLastSnapshot', 'recentErrorSample', 'recent', 'projects', + 'skills', 'agents', 'files', 'modules', 'telemetry', 'memory']) { + if (JSON.stringify(lean).length <= cap) break; + if (key in lean) { delete lean[key]; lean.omitted.push(key); } + } + return lean; + } + + // String form, for prompts. Always valid JSON now. + _modelDigest(maxChars) { + const snap = this._modelSnapshot(maxChars); + return snap ? JSON.stringify(snap) : ''; } // Last few tool/agent errors with their tool name, for the digest (bounded). diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index c3ebe1f..55dde6f 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -3170,7 +3170,10 @@ buildSystemPrompt() { facts: wm._answerQuery(a.question || ''), version: wm._model.version, synthesized: wm._model.synthesized, - snapshot: (a.detail === 'full') ? wm._model : JSON.parse(wm._modelDigest()) + // v4.6.83: this was JSON.parse(wm._modelDigest()) and the digest was a + // string truncated mid-token, so the default path always threw + // "Unexpected end of JSON input". The model now hands back an object. + snapshot: (a.detail === 'full') ? wm._model : wm._modelSnapshot() }; return JSON.stringify(wmOut); } diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 626d7bc..9a373a6 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.82'; + const LATEST_VER = 'v4.6.84'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js index d48327b..54a17e4 100644 --- a/modules/core/module-world-model.js +++ b/modules/core/module-world-model.js @@ -446,30 +446,53 @@ class WorldModelModule { // 2026-10-21: ask() used to be a keyword table only, so any real question got // "Unknown query". Whitelist the fields we are willing to send to a model // (never the raw model object: it is persisted alongside other VFS data). - _modelDigest(maxChars) { + // v4.6.83: the digest used to be a JSON string cut at 4000 chars with '…(digest truncated)' + // glued on. Every caller that tried to parse it got "Unexpected end of JSON input" — the + // world_model_query tool did exactly that on its default path. A digest must shrink, never + // break: this returns a valid OBJECT and the string form is only produced at the edge. + _modelSnapshot(maxChars) { const m = this._model; - if (!m) return ''; + if (!m) return null; const cap = maxChars || 4000; - const pick = { + const build = (keep) => ({ version: m.version, synthesized: m.synthesized, system: m.system, modules: m.modules, files: m.files, - skills: { count: m.skills && m.skills.count, skills: ((m.skills && m.skills.skills) || []).slice(0, 40) }, + skills: { count: m.skills && m.skills.count, skills: ((m.skills && m.skills.skills) || []).slice(0, keep) }, memory: m.memory, agents: m.agents, telemetry: m.telemetry, projects: m.projects, - recent: (m.recent || []).slice(0, 20), + recent: (m.recent || []).slice(0, Math.min(keep, 20)), // 2026-10-21: the model only ever exposed a COUNT ("9 recent errors") with no detail, // so neither the user nor the LLM could say what failed. Carry the last few. recentErrorSample: this._errorSample ? this._errorSample() : [], changesSinceLastSnapshot: this._diffSincePrev ? this._diffSincePrev() : [] - }; - let json = JSON.stringify(pick); - if (json.length > cap) json = json.slice(0, cap) + '…(digest truncated)'; - return json; + }); + // Shrink the lists until the snapshot fits. Truncating the JSON text is what broke it. + for (const keep of [40, 20, 10, 5, 3, 1, 0]) { + const pick = build(keep); + if (JSON.stringify(pick).length <= cap) return pick; + } + // Last resort: drop whole sections, in order of least value, until it fits. Still a + // valid object — a caller can always JSON.parse it and see what was left out. + const lean = build(0); + lean.truncated = true; + lean.omitted = []; + for (const key of ['changesSinceLastSnapshot', 'recentErrorSample', 'recent', 'projects', + 'skills', 'agents', 'files', 'modules', 'telemetry', 'memory']) { + if (JSON.stringify(lean).length <= cap) break; + if (key in lean) { delete lean[key]; lean.omitted.push(key); } + } + return lean; + } + + // String form, for prompts. Always valid JSON now. + _modelDigest(maxChars) { + const snap = this._modelSnapshot(maxChars); + return snap ? JSON.stringify(snap) : ''; } // Last few tool/agent errors with their tool name, for the digest (bounded). |