aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-27 15:23:38 +0100
committerAgando <roduccomm@gmail.com>2026-10-27 15:23:38 +0100
commit6b4049d3ba27dd68935c0565cccfe44752175393 (patch)
treea336032a1c463dddb53c06e20fd7cefb12f721b7
parent6f2255946a834f2458613450533cd2906eb31e7d (diff)
fix(newos4): help names the owner of an undocumented command; loop lessons leave /skills
Two board items plus the context-window close-out. - MAINT-019 (module-system.js): `help <cmd>` answered "No help available for X" for a command that is registered but has no help line, which reads as if the command does not exist. It now says "X — registered by <Module>, which publishes no help line for it. Try: help". A command that genuinely does not exist still gets the old message. - MAINT-020 (module-aiassist.js): auto-generated loop lessons were written into /skills with their own index entries, so `skill list` filled up with system noise. Lessons now live in /lessons with /lessons/index.json; the reader still falls back to legacy loop-lesson-* entries in /skills for one release, and the one-time migration deletes them from the skills index. Verification: test-help-and-lessons.js (10 asserts, includes a lint-style guard that no lesson is written into /skills). Suite 14/14 files green; maintain --fix "node tests 14/14 pass", "help coverage 362/362". Live on v4.6.108: `help agent-register` -> "registered by AgentRegistryModule, which publishes no help line for it"; `help a2a-prune` unchanged; `help nope-not-real` still "No help available". MAINT-001 close-out (no code change here): history is capped at 20 messages with a "[context trim]" marker (first trim at turn 11 in a 60-turn probe), token compression took 80 msgs/8080 tok to 17/2625, and since v4.6.107 one turn's tool bodies are clipped to 6 000 chars each and elided past 24 000 with the newest 4 kept verbatim. Residual, recorded on the board: maxContextTokens is never sent to the server as num_ctx, so the real window is whatever the endpoint defaults to. MAINT-018 close-out: not reproducible. Live probes on v4.6.107/108 — `sed -n '1,3p' /probe.txt` prints lines 1-3, `sed -n "2,4p"` prints 2-4, `grep 'line4'` matches, and the same commands inside `script /q.sh` print correctly. The "Command not found: for;" symptom belongs to the line-by-line script runner, which is MAINT-021 and stays open.
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js53
-rw-r--r--modules/core/module-aiassist.js47
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-system.js6
-rw-r--r--modules/test/test-help-and-lessons.js75
6 files changed, 151 insertions, 38 deletions
diff --git a/main.html b/main.html
index cf782e4..79c77cd 100644
--- a/main.html
+++ b/main.html
@@ -1349,13 +1349,13 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=v4.6.107"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.108"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=v4.6.107"></script>
+<script src="modules/core/module-loader.js?v=v4.6.108"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.107"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.108"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index c06af5f..d7d9d61 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -6431,7 +6431,11 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
if (helpEntry) {
this.terminal.print(helpEntry, 'info');
} else {
- this.terminal.print(`No help available for ${cmd}`, 'warning');
+ // MAINT-019: a registered command with no help line answered "No help
+ // available", which reads as if the command does not exist. Name the
+ // module that owns it so the user knows where to look.
+ const owner = allCommands[cmd];
+ this.terminal.print(`${cmd} — registered by ${owner}, which publishes no help line for it. Try: help`, 'warning');
}
} else {
this.terminal.print(`No help available for ${cmd}`, 'warning');
@@ -10045,13 +10049,21 @@ buildSystemPrompt() {
try {
var fs = this.terminal.fs;
if (fs) {
- var index = fs.fsReadJSON('/skills/index.json') || {};
- var lessonKeys = Object.keys(index).filter(function(k) { return k.indexOf('loop-lesson-') === 0; });
+ // MAINT-020: lessons live in /lessons, not in /skills — writing them into
+ // the skills directory made auto-generated lessons show up as skills.
+ var index = fs.fsReadJSON('/lessons/index.json') || {};
+ var lessonKeys = Object.keys(index);
+ if (lessonKeys.length === 0) {
+ // one-release fallback for lessons written by older builds
+ var legacy = fs.fsReadJSON('/skills/index.json') || {};
+ lessonKeys = Object.keys(legacy).filter(function(k) { return k.indexOf('loop-lesson-') === 0; });
+ }
if (lessonKeys.length > 0) {
var recentLessons = lessonKeys.sort().slice(-3);
var lessonTexts = [];
for (var li = 0; li < recentLessons.length; li++) {
- var lc = fs.readFile('/skills/' + recentLessons[li] + '.md');
+ var lc = fs.readFile('/lessons/' + recentLessons[li] + '.md')
+ || fs.readFile('/skills/' + recentLessons[li] + '.md');
if (lc) {
var excerpt = lc.substring(0, 300);
lessonTexts.push('[Lesson: ' + recentLessons[li] + '] ' + excerpt);
@@ -13187,23 +13199,30 @@ buildSystemPrompt() {
var suggestion = suggestions.join('\n');
try {
var fs = this.terminal.fs;
- var index = fs.fsReadJSON('/skills/index.json') || {};
- var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf('auto-improvement') !== -1; });
- // One-time cleanup: remove all existing loop-lesson files on first run
+ // MAINT-020: lessons are kept in /lessons with their own index; /skills is
+ // reserved for real skills the user or the agent creates deliberately.
+ try { fs.mkdirRecursive('/lessons'); } catch (ex) {}
+ var index = fs.fsReadJSON('/lessons/index.json') || {};
+ var autoSkills = Object.entries(index);
+ // One-time migration: earlier builds wrote loop-lesson-* into /skills.
if (!this._loopLessonCleaned) {
this._loopLessonCleaned = true;
- autoSkills.forEach(function(e) {
- try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
- delete index[e[0]];
- });
- autoSkills = [];
+ try {
+ var sIdx = fs.fsReadJSON('/skills/index.json') || {};
+ Object.keys(sIdx).filter(function(k) { return k.indexOf('loop-lesson-') === 0; })
+ .forEach(function(k) {
+ try { fs.unlink('/skills/' + k + '.md'); } catch (ex) {}
+ delete sIdx[k];
+ });
+ fs.fsWriteJSON('/skills/index.json', sIdx);
+ } catch (ex) {}
}
// Age limit: remove lessons older than 24h
var now = Date.now();
autoSkills = autoSkills.filter(function(e) {
var created = new Date(e[1].created).getTime();
if (now - created > 86400000) {
- try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
+ try { fs.unlink('/lessons/' + e[0] + '.md'); } catch (ex) {}
delete index[e[0]];
return false;
}
@@ -13215,17 +13234,17 @@ buildSystemPrompt() {
// FIFO rotation: if at cap, delete oldest before creating
if (!isDuplicate && autoSkills.length >= 3) {
var oldest = autoSkills[0];
- try { fs.unlink('/skills/' + oldest[0] + '.md'); } catch (ex) {}
+ try { fs.unlink('/lessons/' + oldest[0] + '.md'); } catch (ex) {}
delete index[oldest[0]];
autoSkills.shift();
}
if (!isDuplicate) {
var skillName = 'loop-lesson-' + Date.now();
var content = '---\nname: ' + skillName + '\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n' + JSON.stringify(anomalies, null, 2) + '\n\n## Suggestions\n\n' + suggestion + '\n\n## Context\n- Profile: ' + (this._activeLoopProfile ? this._activeLoopProfile.name : 'none') + '\n- Turns: ' + result.toolTurns + '\n- Tools used: ' + toolLog.map(function(t) { return t.name; }).join(', ');
- fs.mkdirRecursive('/skills');
- fs.writeFile('/skills/' + skillName + '.md', content);
+ fs.mkdirRecursive('/lessons');
+ fs.writeFile('/lessons/' + skillName + '.md', content);
index[skillName] = { name: skillName, description: 'Auto-saved loop lesson', tags: ['auto-improvement', 'agent-loop', 'auto'], category: 'system', created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes };
- fs.fsWriteJSON('/skills/index.json', index);
+ fs.fsWriteJSON('/lessons/index.json', index);
}
} catch (e) { /* VFS not available */ }
var msg = '💡 Self-improvement: ' + anomalies.length + ' anomaly(ies)';
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 9965e6a..be5ebce 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -1530,13 +1530,21 @@ buildSystemPrompt() {
try {
var fs = this.terminal.fs;
if (fs) {
- var index = fs.fsReadJSON('/skills/index.json') || {};
- var lessonKeys = Object.keys(index).filter(function(k) { return k.indexOf('loop-lesson-') === 0; });
+ // MAINT-020: lessons live in /lessons, not in /skills — writing them into
+ // the skills directory made auto-generated lessons show up as skills.
+ var index = fs.fsReadJSON('/lessons/index.json') || {};
+ var lessonKeys = Object.keys(index);
+ if (lessonKeys.length === 0) {
+ // one-release fallback for lessons written by older builds
+ var legacy = fs.fsReadJSON('/skills/index.json') || {};
+ lessonKeys = Object.keys(legacy).filter(function(k) { return k.indexOf('loop-lesson-') === 0; });
+ }
if (lessonKeys.length > 0) {
var recentLessons = lessonKeys.sort().slice(-3);
var lessonTexts = [];
for (var li = 0; li < recentLessons.length; li++) {
- var lc = fs.readFile('/skills/' + recentLessons[li] + '.md');
+ var lc = fs.readFile('/lessons/' + recentLessons[li] + '.md')
+ || fs.readFile('/skills/' + recentLessons[li] + '.md');
if (lc) {
var excerpt = lc.substring(0, 300);
lessonTexts.push('[Lesson: ' + recentLessons[li] + '] ' + excerpt);
@@ -4672,23 +4680,30 @@ buildSystemPrompt() {
var suggestion = suggestions.join('\n');
try {
var fs = this.terminal.fs;
- var index = fs.fsReadJSON('/skills/index.json') || {};
- var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf('auto-improvement') !== -1; });
- // One-time cleanup: remove all existing loop-lesson files on first run
+ // MAINT-020: lessons are kept in /lessons with their own index; /skills is
+ // reserved for real skills the user or the agent creates deliberately.
+ try { fs.mkdirRecursive('/lessons'); } catch (ex) {}
+ var index = fs.fsReadJSON('/lessons/index.json') || {};
+ var autoSkills = Object.entries(index);
+ // One-time migration: earlier builds wrote loop-lesson-* into /skills.
if (!this._loopLessonCleaned) {
this._loopLessonCleaned = true;
- autoSkills.forEach(function(e) {
- try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
- delete index[e[0]];
- });
- autoSkills = [];
+ try {
+ var sIdx = fs.fsReadJSON('/skills/index.json') || {};
+ Object.keys(sIdx).filter(function(k) { return k.indexOf('loop-lesson-') === 0; })
+ .forEach(function(k) {
+ try { fs.unlink('/skills/' + k + '.md'); } catch (ex) {}
+ delete sIdx[k];
+ });
+ fs.fsWriteJSON('/skills/index.json', sIdx);
+ } catch (ex) {}
}
// Age limit: remove lessons older than 24h
var now = Date.now();
autoSkills = autoSkills.filter(function(e) {
var created = new Date(e[1].created).getTime();
if (now - created > 86400000) {
- try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
+ try { fs.unlink('/lessons/' + e[0] + '.md'); } catch (ex) {}
delete index[e[0]];
return false;
}
@@ -4700,17 +4715,17 @@ buildSystemPrompt() {
// FIFO rotation: if at cap, delete oldest before creating
if (!isDuplicate && autoSkills.length >= 3) {
var oldest = autoSkills[0];
- try { fs.unlink('/skills/' + oldest[0] + '.md'); } catch (ex) {}
+ try { fs.unlink('/lessons/' + oldest[0] + '.md'); } catch (ex) {}
delete index[oldest[0]];
autoSkills.shift();
}
if (!isDuplicate) {
var skillName = 'loop-lesson-' + Date.now();
var content = '---\nname: ' + skillName + '\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n' + JSON.stringify(anomalies, null, 2) + '\n\n## Suggestions\n\n' + suggestion + '\n\n## Context\n- Profile: ' + (this._activeLoopProfile ? this._activeLoopProfile.name : 'none') + '\n- Turns: ' + result.toolTurns + '\n- Tools used: ' + toolLog.map(function(t) { return t.name; }).join(', ');
- fs.mkdirRecursive('/skills');
- fs.writeFile('/skills/' + skillName + '.md', content);
+ fs.mkdirRecursive('/lessons');
+ fs.writeFile('/lessons/' + skillName + '.md', content);
index[skillName] = { name: skillName, description: 'Auto-saved loop lesson', tags: ['auto-improvement', 'agent-loop', 'auto'], category: 'system', created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes };
- fs.fsWriteJSON('/skills/index.json', index);
+ fs.fsWriteJSON('/lessons/index.json', index);
}
} catch (e) { /* VFS not available */ }
var msg = '💡 Self-improvement: ' + anomalies.length + ' anomaly(ies)';
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 3204af0..836672d 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.107';
+ const LATEST_VER = 'v4.6.108';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/module-system.js b/modules/core/module-system.js
index 40b0861..43bb0b1 100644
--- a/modules/core/module-system.js
+++ b/modules/core/module-system.js
@@ -743,7 +743,11 @@
if (helpEntry) {
this.terminal.print(helpEntry, 'info');
} else {
- this.terminal.print(`No help available for ${cmd}`, 'warning');
+ // MAINT-019: a registered command with no help line answered "No help
+ // available", which reads as if the command does not exist. Name the
+ // module that owns it so the user knows where to look.
+ const owner = allCommands[cmd];
+ this.terminal.print(`${cmd} — registered by ${owner}, which publishes no help line for it. Try: help`, 'warning');
}
} else {
this.terminal.print(`No help available for ${cmd}`, 'warning');
diff --git a/modules/test/test-help-and-lessons.js b/modules/test/test-help-and-lessons.js
new file mode 100644
index 0000000..c4eb5ae
--- /dev/null
+++ b/modules/test/test-help-and-lessons.js
@@ -0,0 +1,75 @@
+// modules/test/test-help-and-lessons.js — run: node
+// MAINT-019: `help <cmd>` must not answer "No help available" for a command that exists.
+// MAINT-020: loop lessons must be written to /lessons, never into /skills.
+const fsMod = require('fs'), path = require('path');
+const ROOT = path.join(__dirname, '..');
+const store = {};
+global.localStorage = { getItem: k => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; } };
+global.indexedDB = { open() { throw new Error('no idb'); }, deleteDatabase() { throw new Error('no idb'); } };
+global.window = global;
+
+const SystemModule = new Function(fsMod.readFileSync(path.join(ROOT, 'core', 'module-system.js'), 'utf8') + '\nreturn SystemModule;')();
+
+let fails = 0, passes = 0;
+function ok(name, cond, detail) {
+ if (cond) { passes++; return; }
+ fails++;
+ console.log('FAIL ' + name + (detail ? ' — ' + detail : ''));
+}
+
+function mkHelp(commands, help) {
+ const out = [];
+ const sys = Object.create(SystemModule.prototype);
+ sys.terminal = {
+ print: (m, t) => out.push({ text: String(m), type: t || '' }),
+ log: () => {},
+ moduleManager: {
+ getAllCommands: () => commands,
+ getAllHelp: () => help,
+ },
+ };
+ return { sys, out };
+}
+const text = out => out.map(o => o.text).join(' | ');
+
+// A. a documented command prints its help line
+{
+ const { sys, out } = mkHelp({ ls: 'FileSystemModule' }, { ls: 'ls [path] — List files' });
+ sys.handleHelp(['ls']);
+ ok('A1 documented command shows its help', /ls \[path\]/.test(text(out)), text(out));
+}
+
+// B. an undocumented but registered command names its owner instead of denying it exists
+{
+ const { sys, out } = mkHelp({ 'agent-register': 'AgentRegistryModule' }, {});
+ sys.handleHelp(['agent-register']);
+ const t = text(out);
+ ok('B1 no "No help available" for a registered command', !/No help available/.test(t), t);
+ ok('B2 the owning module is named', /AgentRegistryModule/.test(t), t);
+ ok('B3 it still says there is no help line', /no help line/i.test(t), t);
+}
+
+// C. a command that really does not exist still says so
+{
+ const { sys, out } = mkHelp({ ls: 'FileSystemModule' }, { ls: 'ls — List' });
+ sys.handleHelp(['definitely-not-a-command']);
+ ok('C1 unknown command still reports no help', /No help available/.test(text(out)), text(out));
+}
+
+// D. MAINT-020: no lesson is written into /skills, and /lessons is used instead
+{
+ const src = fsMod.readFileSync(path.join(ROOT, 'core', 'module-aiassist.js'), 'utf8');
+ const writesSkils = /writeFile\(\s*'\/skills\/'\s*\+\s*skillName/.test(src);
+ ok('D1 no lesson file is written into /skills', !writesSkils);
+ ok('D2 lesson files are written into /lessons', /writeFile\(\s*'\/lessons\/'\s*\+\s*skillName/.test(src));
+ ok('D3 the lessons index is written to /lessons', /fsWriteJSON\(\s*'\/lessons\/index\.json'/.test(src));
+ ok('D4 the reader still handles legacy /skills lessons', /fs\.readFile\('\/skills\/' \+ recentLessons\[li\] \+ '\.md'\)/.test(src));
+ ok('D5 legacy lessons are cleaned out of the skills index once', /indexOf\('loop-lesson-'\) === 0/.test(src));
+}
+
+if (fails === 0) {
+ console.log('test-help-and-lessons: ' + passes + ' passed, 0 failed');
+} else {
+ console.log('test-help-and-lessons: ' + passes + ' passed, ' + fails + ' FAILED');
+ process.exit(1);
+}