diff options
Diffstat (limited to '.bkp-fix-20261023_143429/module-world-model.js')
| -rw-r--r-- | .bkp-fix-20261023_143429/module-world-model.js | 600 |
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 |