aboutsummaryrefslogtreecommitdiff
path: root/.bkp-fix-20261023_143429/module-world-model.js
diff options
context:
space:
mode:
Diffstat (limited to '.bkp-fix-20261023_143429/module-world-model.js')
-rw-r--r--.bkp-fix-20261023_143429/module-world-model.js600
1 files changed, 600 insertions, 0 deletions
diff --git a/.bkp-fix-20261023_143429/module-world-model.js b/.bkp-fix-20261023_143429/module-world-model.js
new file mode 100644
index 0000000..cbfd1fb
--- /dev/null
+++ b/.bkp-fix-20261023_143429/module-world-model.js
@@ -0,0 +1,600 @@
+// module-world-model.js — Global World Model: system self-awareness synthesis engine
+// Aggregates from memory graph, neural VFS, skills index, telemetry
+// Commands: world-model, world-model-summary, world-model-ask (LLM), world-model-refresh, world-model-config
+class WorldModelModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._model = null;
+ this._lastRefresh = null;
+ this._REFRESH_INTERVAL = 300000; // 5 minutes (fallback)
+ this._MAX_PROJECTS = 10;
+ this._refreshTimer = null;
+ // v4.5.89: Event-driven sync state
+ this._syncDebounce = null;
+ this._syncing = false;
+ this._lastEventSync = 0;
+ this._EVENT_DEBOUNCE_MS = 500;
+ }
+
+ getHelpInfo() {
+ return {
+ 'world-model': 'world-model — Show full world model as structured output',
+ 'world-model-summary': 'world-model-summary — One-line system self-awareness statement',
+'world-model-ask': 'world-model-ask <question> — Ask the LLM about this system (grounded in the world model; --facts for the instant keyword lookup)',
+ 'world-model-refresh': 'world-model-refresh — Force re-synthesis from all subsystems',
+ 'world-model-config': 'world-model-config <key> <value> — Tune: refresh-interval, max-projects',
+ };
+ }
+
+ registerCommands() {
+ this.commands['world-model'] = () => this.showModel();
+ this.commands['world-model-summary'] = () => this.showSummary();
+ this.commands['world-model-ask'] = (args) => this.ask(args.join(' '));
+ this.commands['world-model-refresh'] = () => this.refresh();
+ this.commands['world-model-config'] = (args) => this.setConfig(args);
+ }
+
+ init(terminal) {
+ this.terminal = terminal;
+ this._loadFromVFS();
+ // Deferred initial synthesis — wait for ext modules (SkillPackCore etc) to fully load
+ setTimeout(() => {
+ this._synthesize();
+ this.terminal.print('🌐 World Model synthesized — system self-awareness active', 'info');
+ // Second pass after ext modules definitely finished loading
+ setTimeout(() => {
+ this._synthesize();
+ this.terminal.print('🌐 World Model refresh — full module load confirmed', 'info');
+ }, 7000);
+ }, 5000);
+ // Auto-refresh on a timer
+ this._refreshTimer = setInterval(() => {
+ const age = Date.now() - (this._lastRefresh ? new Date(this._lastRefresh).getTime() : 0);
+ if (age > this._REFRESH_INTERVAL) this._synthesize();
+ }, this._REFRESH_INTERVAL);
+ this.terminal.print('🌐 World Model loaded — system self-awareness active', 'info');
+ }
+
+ // v4.5.89: Event-driven sync — debounced, recursion-guarded
+ _scheduleSync() {
+ if (this._syncing) return; // Prevent recursion during active synthesis
+ if (this._syncDebounce) clearTimeout(this._syncDebounce);
+ const now = Date.now();
+ // Skip if we synced < 2s ago (rate limit beyond debounce)
+ if (now - this._lastEventSync < 2000) return;
+ this._syncDebounce = setTimeout(() => {
+ this._syncDebounce = null;
+ this._syncing = true;
+ try {
+ this._synthesize();
+ this._lastEventSync = Date.now();
+ } finally {
+ this._syncing = false;
+ }
+ }, this._EVENT_DEBOUNCE_MS);
+ }
+
+ // ─── Synthesis Engine ───
+
+ _synthesize() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+
+ // Derive version from actual system state — no hardcoded strings
+ const mInfo = this._gatherModuleInfo();
+ const agentInfo = this._gatherAgentInfo();
+ const fileInfo = this._gatherFileInfo(fs);
+ const skillInfo = this._gatherSkillInfo(fs);
+ const liveVersion = this._deriveVersion(mInfo);
+
+ const model = {
+ version: liveVersion,
+ synthesized: new Date().toISOString(),
+ system: this._gatherSystemInfo(fs, mInfo.loaded, liveVersion),
+ modules: mInfo,
+ files: fileInfo,
+ skills: skillInfo,
+ memory: this._gatherMemoryInfo(),
+ agents: agentInfo,
+ telemetry: this._gatherTelemetry(fs),
+ projects: this._gatherProjects(fs),
+ recent: this._gatherRecent(fs)
+ };
+
+ this._model = model;
+ this._lastRefresh = model.synthesized;
+ this._saveToVFS(fs);
+
+ // Log synthesis summary
+ const mCount = model.modules?.loaded || '?';
+ const sCount = model.skills?.count || '?';
+ const fCount = model.files?.total || '?';
+ console.log(`[WorldModel] Synthesis complete: ${mCount} modules, ${sCount} skills, ${fCount} files`);
+ } catch (e) {
+ console.error('[WorldModel] Synthesis error:', e.message);
+ }
+ }
+
+ _deriveVersion(mInfo) {
+ // Single source of truth: the constant set at bootstrap
+ if (window.NEWOS4_VERSION) return window.NEWOS4_VERSION;
+ // Fallback: try to extract from script tag ?v= query param or LATEST_VER constant
+ try {
+ const scripts = document.querySelectorAll('script[src*="module-loader"]');
+ for (const s of scripts) {
+ const match = s.src.match(/[?&]v=([^&]+)/);
+ if (match) return match[1];
+ }
+ // Try to find LATEST_VER in any inline script
+ const allScripts = document.querySelectorAll('script:not([src])');
+ for (const s of allScripts) {
+ const match = s.textContent.match(/LATEST_VER\s*=\s*['"]([^'"]+)['"]/);
+ if (match) return match[1];
+ }
+ } catch (e) {
+ console.warn('[WorldModel] Version extraction error:', e.message);
+ }
+ return 'v?';
+ }
+
+ _getTotalModules() {
+ try {
+ const mm = this.terminal?.moduleManager;
+ if (!mm) return 0;
+ // Use max of registry vs loaded so the ratio is never >1 (e.g. 43/7 → 43/43)
+ const loadedCount = Object.keys(mm.modules || {}).length;
+ const registryCount = (mm.moduleRegistry || []).length;
+ return Math.max(loadedCount, registryCount);
+ } catch (e) {
+ console.warn('[WorldModel] Total modules gather error:', e.message);
+ return Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0;
+ }
+ }
+
+ _gatherSystemInfo(fs, moduleCount, version) {
+ // Get real health data from ServerMonitorModule's _latencyLog and DebugSupervisorModule
+ const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
+ const errorCount = ds ? Object.keys(ds._errorDB?.errors || {}).length : 0;
+ const latencyErrors = sm?._latencyLog ? Object.values(sm._latencyLog).filter(e => e.status === 'error').length : 0;
+ const totalErrors = errorCount + latencyErrors;
+ return {
+ version: `newOS4 ${version}`,
+ modules: moduleCount || Object.keys(this.terminal?.moduleManager?.modules || {}).length || 0,
+ vfsSize: JSON.stringify(fs?.root || {}).length,
+ online: typeof navigator !== 'undefined' ? navigator.onLine : true,
+ memory: typeof performance !== 'undefined' && performance.memory
+ ? `${(performance.memory.usedJSHeapSize / 1048576).toFixed(1)}MB`
+ : 'N/A',
+ health: totalErrors > 0 ? `⚠️ ${totalErrors} errors` : 'OK',
+ lastHealthCheck: new Date().toISOString()
+ };
+ }
+
+ _gatherModuleInfo() {
+ const mm = this.terminal?.moduleManager;
+ if (!mm) return { loaded: 0, modified: [] };
+ const modules = Object.keys(mm.modules || {}).map(name => {
+ const mod = mm.modules[name];
+ const clsName = mod?.constructor?.name || name;
+ return { name: clsName, hasCommands: !!mod?.commands };
+ });
+ // Check for local modifications
+ const modsDir = this.terminal.fs?.readDir('/_mods/');
+ const modified = modsDir
+ ? modsDir.filter(e => e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js'))
+ .map(e => e.name.replace('.js', ''))
+ : [];
+ return {
+ loaded: modules.length,
+ total: this._getTotalModules(),
+ modules,
+ modified,
+ modifiedCount: modified.length
+ };
+ }
+
+ _gatherFileInfo(fs) {
+ if (!fs) return { total: 0, size: '0KB' };
+ let fileCount = 0, dirCount = 0;
+ const walk = (path) => {
+ const entries = fs.ls(path);
+ if (!entries) return;
+ for (const e of entries) {
+ if (e.type === 'file') fileCount++;
+ else { dirCount++; walk(path === '/' ? '/' + e.name : path + '/' + e.name); }
+ }
+ };
+ walk('/');
+ return {
+ total: fileCount,
+ directories: dirCount,
+ size: `${(JSON.stringify(fs.root).length / 1024).toFixed(1)}KB`
+ };
+ }
+
+ _gatherSkillInfo(fs) {
+ try {
+ const idx = fs.fsReadJSON('/skills/index.json');
+ const count = idx ? Object.keys(idx).length : 0;
+ return { count, skills: idx ? Object.keys(idx) : [] };
+ } catch (e) {
+ console.warn('[WorldModel] Skill info gather error:', e.message);
+ return { count: 0, skills: [] };
+ }
+ }
+
+ _gatherMemoryInfo() {
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ if (mg && mg._nodes) {
+ const nodes = Object.keys(mg._nodes).length;
+ const edges = (mg._edges || []).length;
+ return { nodes, edges, active: true, fallback: false };
+ }
+ // Fallback: count memory files
+ try {
+ const memIndex = this.terminal.fs?.fsReadJSON('/memory/index.json');
+ return { nodes: memIndex ? Object.keys(memIndex).length : 0, edges: 0, active: false, fallback: true };
+ } catch (e) {
+ console.warn('[WorldModel] Memory info gather error:', e.message);
+ return { nodes: 0, edges: 0, active: false, fallback: true };
+ }
+ }
+
+ _gatherAgentInfo() {
+ const orch = this.terminal?.moduleManager?.modules?.['OrchestratorModule'];
+ if (!orch || !orch._agents) return { agents: -1, active: -1, error: 'Orchestrator not loaded' };
+ const agents = Object.keys(orch._agents).length;
+ const active = Object.values(orch._agents).filter(a => a.status === 'busy').length;
+ return { agents, active, totalLeases: Object.keys(orch._a2aMessages || {}).length };
+ }
+
+ _gatherTelemetry(fs) {
+ try {
+ // Get real latency data from ServerMonitorModule and errors from DebugSupervisorModule
+ const sm = this.terminal?.moduleManager?.modules?.['ServerMonitorModule'];
+ const ds = this.terminal?.moduleManager?.modules?.['DebugSupervisorModule'];
+ const latencyDataPoints = sm?._latencyLog ? Object.keys(sm._latencyLog).length : 0;
+ const recentErrors = ds ? Object.keys(ds._errorDB?.errors || {}).length : 0;
+ return {
+ toolLatencyDataPoints: latencyDataPoints,
+ recentErrors: recentErrors,
+ };
+ } catch (e) {
+ console.warn('[WorldModel] Telemetry gather error:', e.message);
+ return { toolLatencyDataPoints: 0, recentErrors: 0 };
+ }
+ }
+
+ _gatherProjects(fs) {
+ try {
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ if (mg && mg._nodes) {
+ // Find task clusters as "projects"
+ const tasks = Object.values(mg._nodes).filter(n => n.type === 'task');
+ const recentFiles = Object.values(mg._nodes)
+ .filter(n => n.type === 'file')
+ .sort((a, b) => (b.access_count || 0) - (a.access_count || 0))
+ .slice(0, this._MAX_PROJECTS);
+ return {
+ totalTasks: tasks.length,
+ activeProjects: recentFiles.slice(0, 5).map(f => ({
+ name: f.label,
+ accesses: f.access_count || 0,
+ lastModified: f.updated
+ }))
+ };
+ }
+ return { totalTasks: 0, activeProjects: [] };
+ } catch (e) {
+ console.warn('[WorldModel] Projects gather error:', e.message);
+ return { totalTasks: 0, activeProjects: [] };
+ }
+ }
+
+ _gatherRecent(fs) {
+ const recent = [];
+ try {
+ // Check modified modules
+ const modsDir = fs.readDir('/_mods/');
+ if (modsDir) {
+ for (const e of modsDir) {
+ if (e.type === 'file' && e.name.endsWith('.js') && !e.name.endsWith('.orig.js') && !e.name.endsWith('.meta.json')) {
+ recent.push(`Modified module: ${e.name.replace('.js', '')}`);
+ }
+ }
+ }
+ // Check skills activity
+ const skills = fs.ls('/skills/');
+ if (skills && skills.length > 0) {
+ recent.push(`Skills directory: ${skills.filter(s => s.type === 'file').length} files`);
+ }
+ // Check memory directory for recent changes
+ const memDir = fs.readDir('/memory/');
+ if (memDir && memDir.length > 0) {
+ const memFiles = memDir.filter(s => s.type === 'file');
+ if (memFiles.length > 0) {
+ recent.push(`Memory: ${memFiles.length} files in /memory/`);
+ }
+ }
+ // Check system config changes
+ const sysDir = fs.readDir('/system/');
+ if (sysDir && sysDir.length > 0) {
+ const configFiles = sysDir.filter(s => s.type === 'file' && s.name.endsWith('.json'));
+ if (configFiles.length > 0) {
+ recent.push(`System config: ${configFiles.length} config files`);
+ }
+ }
+ } catch (e) {
+ console.warn('[WorldModel] Recent gather error:', e.message);
+ }
+ return recent;
+ }
+
+ // ─── Commands ───
+
+ showModel() {
+ if (!this._model) return this.terminal.print('No model yet — run world-model-refresh', 'warning');
+ const m = this._model;
+
+ this.terminal.print('🌐 World Model', 'header');
+ this.terminal.print(` Version: ${m.version} (last synthsized: ${new Date(m.synthesized).toLocaleTimeString()})`, 'info');
+
+ this.terminal.print(` ⚙️ System — ${m.system.modules} modules loaded, ${m.system.vfsSize} VFS, ${m.system.memory}`, 'info');
+ this.terminal.print(` 🧩 Modules — ${m.modules.loaded} loaded (${m.modules.modifiedCount} modified)`, 'info');
+ this.terminal.print(` 📁 Files — ${m.files.total} files, ${m.files.directories} dirs, ${m.files.size}`, 'info');
+ this.terminal.print(` 📚 Skills — ${m.skills.count} installed`, 'info');
+ this.terminal.print(` 🧠 Memory Graph — ${m.memory.nodes} nodes, ${m.memory.edges} edges${m.memory.active ? ' ✅' : ''}`, 'info');
+ this.terminal.print(` 🤖 Agents — ${m.agents.agents} registered (${m.agents.active} active)`, 'info');
+ this.terminal.print(` 📊 Telemetry — ${m.telemetry.toolLatencyDataPoints} tool latency points, ${m.telemetry.recentErrors} recent errors`, 'info');
+
+ if (m.projects.activeProjects.length > 0) {
+ this.terminal.print(' Active context:', 'header');
+ for (const p of m.projects.activeProjects.slice(0, 3)) {
+ this.terminal.print(` 📌 ${p.name} (${p.accesses}x accesses)`, 'info');
+ }
+ }
+
+ if (m.recent.length > 0) {
+ this.terminal.print(' Recent changes:', 'header');
+ for (const r of m.recent.slice(0, 3)) {
+ this.terminal.print(` ${r}`, 'info');
+ }
+ }
+ }
+
+ showSummary() {
+ if (!this._model) return this.terminal.print('No model yet', 'warning');
+ const m = this._model;
+ const summary = `newOS4 ${m.version} · ${m.modules.loaded}/${m.modules.total} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents · ${m.memory.nodes} graph nodes · ${m.telemetry.recentErrors > 0 ? `⚠️ ${m.telemetry.recentErrors} errors` : '✅ OK'}`;
+ this.terminal.print(summary, 'info');
+ return summary;
+ }
+
+ async ask(question) {
+ if (!question) return this.terminal.print('Usage: world-model-ask <question> (--facts = fast keyword lookup)', 'error');
+ const factsOnly = /(^|\s)--facts\b/.test(question);
+ if (factsOnly) question = question.replace(/--facts\b/, '').trim();
+ if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning');
+
+ this.terminal.print(`❓ ${question}`, 'info');
+
+ if (!factsOnly) {
+ const om = this.terminal.ollamaManager;
+ try {
+ if (om && !om.connected) { try { await om.connect(); } catch (e) {} }
+ if (om && om.connected) {
+ const answer = await om.chat(this._buildAskPrompt(question));
+ if (answer && String(answer).trim()) {
+ this.terminal.print(`💡 ${String(answer).trim()}`, 'success');
+ return;
+ }
+ }
+ } catch (e) { /* fall through to the facts path */ }
+ this.terminal.print('⚠️ LLM unavailable — showing the factual lookup instead', 'warning');
+ }
+
+ this.terminal.print(`💡 ${this._answerQuery(question)}`, 'success');
+ }
+
+ // Shared query routing — used by both ask() (terminal) and world_model_query (AI tool)
+ _answerQuery(question) {
+ if (!this._model) return 'World model not synthesized yet — run world-model-refresh';
+ const q = (question || '').toLowerCase();
+ const m = this._model;
+
+ if (q.includes('module') || q.includes('loaded')) {
+ return `Loaded ${m.modules.loaded} modules (${m.modules.modifiedCount} modified: ${m.modules.modified.join(', ') || 'none'})`;
+ } else if (q.includes('skill')) {
+ return `Installed ${m.skills.count} skills: ${m.skills.skills.join(', ') || 'none'}`;
+ } else if (q.includes('file') || q.includes('vfs')) {
+ return `${m.files.total} files in ${m.files.directories} directories, total ${m.files.size}`;
+ } else if (q.includes('agent') || q.includes('orchestrat')) {
+ return `${m.agents.agents} agents (${m.agents.active} active)`;
+ } else if (q.includes('health') || q.includes('status')) {
+ return `Health: ${m.system.health} · Memory: ${m.system.memory} · ${m.telemetry.recentErrors > 0 ? `${m.telemetry.recentErrors} recent errors` : 'No errors'}`;
+ } else if (q.includes('memory') || q.includes('graph')) {
+ return `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`;
+ } else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) {
+ const diff = this._diffSincePrev();
+ const recent = m.recent || [];
+ const parts = [];
+ if (diff.length) parts.push('Since the last snapshot: ' + diff.join('; '));
+ if (recent.length) parts.push('Current: ' + recent.join('; '));
+ return parts.length ? parts.join(' · ') : 'No previous snapshot yet — run world-model-refresh twice';
+ } else if (q.includes('project') || q.includes('work')) {
+ const projects = m.projects.activeProjects;
+ return projects.length > 0
+ ? `Active projects: ${projects.map(p => `${p.name} (${p.accesses}x)`).join(', ')}`
+ : 'No active projects detected';
+ } else if (q.includes('telemetry') || q.includes('latency')) {
+ return `${m.telemetry.toolLatencyDataPoints} tool latency data points tracked, ${m.telemetry.recentErrors} recent errors`;
+ } else if (q.includes('summary') || q.includes('overview') || q.includes('all')) {
+ return `newOS4 ${m.version} · ${m.modules.loaded}/${m.modules.total} modules · ${m.skills.count} skills · ${m.files.total} files · ${m.agents.agents} agents · ${m.memory.nodes} graph nodes · ${m.telemetry.recentErrors > 0 ? `⚠️ ${m.telemetry.recentErrors} errors` : '✅ OK'}`;
+ } else {
+ return `Unknown query. Try: modules, skills, files, agents, health, memory, changes, projects, telemetry, or summary.`;
+ }
+ }
+
+ // 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) {
+ const m = this._model;
+ if (!m) return '';
+ const cap = maxChars || 4000;
+ const pick = {
+ 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) },
+ memory: m.memory,
+ agents: m.agents,
+ telemetry: m.telemetry,
+ projects: m.projects,
+ recent: (m.recent || []).slice(0, 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;
+ }
+
+ // Last few tool/agent errors with their tool name, for the digest (bounded).
+ _errorSample() {
+ try {
+ const ds = this.terminal.moduleManager.modules['DebugSupervisorModule'];
+ const db = ds && ds._errorDB && ds._errorDB.errors;
+ if (!db) return [];
+ return Object.keys(db).slice(-5).map(k => {
+ const e = db[k] || {};
+ return String(e.tool || e.module || k).slice(0, 40) + ': ' + String(e.error || e.message || '').slice(0, 80);
+ });
+ } catch (e) { return []; }
+ }
+
+ _buildAskPrompt(question) {
+ return [
+ 'You are the newOS4 world model interface. Answer ONLY from the JSON snapshot below.',
+ 'If the snapshot does not contain the answer, say what is missing and name the command that',
+ 'would produce it (world-model-refresh, world-model, modules, test). Never invent numbers.',
+ 'Answer in 1-3 short sentences, plain text, no markdown headers.',
+ '',
+ 'SNAPSHOT: ' + this._modelDigest(),
+ '',
+ 'QUESTION: ' + String(question || '').trim()
+ ].join('\n');
+ }
+
+ _diffSincePrev() {
+ try {
+ const fs = this.terminal.fs;
+ const prev = fs && fs.fsReadJSON('/system/world-model.prev.json');
+ const cur = this._model;
+ if (!prev || !cur) return [];
+ const out = [];
+ if (prev.version && cur.version && prev.version !== cur.version) out.push(`version ${prev.version} → ${cur.version}`);
+ const fields = [['modules', 'loaded'], ['skills', 'count'], ['files', 'total'], ['agents', 'agents'], ['memory', 'nodes']];
+ fields.forEach(function (f) {
+ const a = prev[f[0]] && prev[f[0]][f[1]];
+ const b = cur[f[0]] && cur[f[0]][f[1]];
+ if (typeof a === 'number' && typeof b === 'number' && a !== b) out.push(`${f[0]} ${a} → ${b}`);
+ });
+ if (prev.synthesized) out.push('previous snapshot: ' + prev.synthesized);
+ return out;
+ } catch (e) { return []; }
+ }
+
+ refresh() {
+ this._synthesize();
+ this.terminal.print(`✅ World Model refreshed at ${new Date().toLocaleTimeString()}`, 'success');
+ }
+
+ setConfig(args) {
+ if (args.length < 2) return this.terminal.print('Usage: world-model-config <key> <value>', 'error');
+ const key = args[0];
+ const val = args[1];
+
+ const configMap = {
+ 'refresh-interval': () => {
+ const n = parseInt(val) * 1000;
+ if (isNaN(n) || n < 10000) return 'Must be >= 10 seconds';
+ this._REFRESH_INTERVAL = n;
+ if (this._refreshTimer) clearInterval(this._refreshTimer);
+ this._refreshTimer = setInterval(() => this._synthesize(), n);
+ return `refresh-interval set to ${val}s`;
+ },
+ 'max-projects': () => {
+ const n = parseInt(val);
+ if (isNaN(n) || n < 1) return 'Must be >= 1';
+ this._MAX_PROJECTS = n;
+ return `max-projects set to ${n}`;
+ },
+ };
+
+ if (!configMap[key]) return this.terminal.print(`Unknown: ${key}. Valid: ${Object.keys(configMap).join(', ')}`, 'error');
+ const msg = configMap[key]();
+ this.terminal.print(`✅ ${msg}`, 'success');
+ }
+
+ // ─── Context Injection ───
+
+ getContextForPrompt() {
+ if (!this._model) return '';
+ const m = this._model;
+ const agentStr = m.agents.error
+ ? 'agents: N/A'
+ : `${m.agents.agents} agents`;
+ return `[World Model Context]\nSystem: newOS4 ${m.version} · ${m.modules.loaded} modules · ${m.skills.count} skills · ${m.files.total} files · ${agentStr}${m.telemetry.recentErrors > 0 ? ` · ⚠️ ${m.telemetry.recentErrors} errors` : ' · ✅ OK'}${m.modules.modifiedCount > 0 ? `\nModified modules: ${m.modules.modified.slice(0, 3).join(', ')}${m.modules.modifiedCount > 3 ? ` (+${m.modules.modifiedCount - 3} more)` : ''}` : ''}`;
+ }
+
+ // ─── Persistence ───
+
+ _loadFromVFS() {
+ try {
+ const fs = this.terminal.fs;
+ if (!fs) return;
+ const data = fs.fsReadJSON('/system/world-model.json');
+ if (data) {
+ this._model = data;
+ this._lastRefresh = data.synthesized;
+ }
+ } catch (e) {
+ console.warn('[WorldModel] Load error (fresh start):', e.message);
+ }
+ }
+
+ // v4.5.89: Atomic write — temp file + rename to prevent corruption
+ _saveToVFS(fs) {
+ try {
+ if (!fs.readDir('/system')) fs.mkdir('system');
+ // 2026-10-21: keep one previous model so "what changed" can be a real diff.
+ const prevModel = fs.fsReadJSON('/system/world-model.json');
+ if (prevModel) { try { fs.fsWriteJSON('/system/world-model.prev.json', prevModel); } catch (_) {} }
+ // v4.5.89b fix: Clean stale temp from previous crash (P1 from review)
+ try { fs.deleteFile('/system/world-model.tmp.json'); } catch (_) {}
+ fs.fsWriteJSON('/system/world-model.tmp.json', this._model);
+ const tmp = fs.fsReadJSON('/system/world-model.tmp.json');
+ if (tmp) {
+ fs.fsWriteJSON('/system/world-model.json', tmp);
+ fs.deleteFile('/system/world-model.tmp.json');
+ } else {
+ console.error('[WorldModel] Save failed: temp file corrupt after write');
+ }
+ } catch (e) {
+ console.error('[WorldModel] Save error:', e.message);
+ }
+ }
+}
+
+// Required for module system
+if (typeof window !== 'undefined') {
+ window.WorldModelModule = WorldModelModule;
+} \ No newline at end of file