aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/core-bundle.js46
-rw-r--r--modules/core/module-aiassist.js5
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-world-model.js41
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).