diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/core/core-bundle.js | 107 | ||||
| -rw-r--r-- | modules/core/module-debug-supervisor.js | 67 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 |
3 files changed, 164 insertions, 12 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 245f2bd..331e090 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -17490,9 +17490,16 @@ class MemoryGraphModule { // v4.5.90b fix: Use configurable _decayDays (P1 from review) const staleThreshold = this._decayDays * 24 * 60 * 60 * 1000; var stale = []; + var untracked = []; for (var name of Object.keys(index)) { var stats = index[name] || {}; if (stats.immune) continue; + // A skill with no usage telemetry has never been measured. Calling it stale is how + // the audit came to recommend archiving the system's entire procedural memory. + if (stats.lastUsed === undefined && stats.useCount === undefined) { + untracked.push(name); + continue; + } var useCount = stats.useCount || 0; var lastUsed = stats.lastUsed ? new Date(stats.lastUsed).getTime() : 0; if (useCount === 0 && (now - lastUsed > staleThreshold || lastUsed === 0)) { @@ -17500,12 +17507,17 @@ class MemoryGraphModule { stale.push({ name: name, daysUnused: daysUnused }); } } + this._untrackedSkills = untracked; return stale; } catch (e) { return []; } } + getUntrackedSkills() { + return Array.isArray(this._untrackedSkills) ? this._untrackedSkills : []; + } + // v4.5.89: Configure decay policy decayConfig(args) { if (args.length < 2) { @@ -18550,14 +18562,28 @@ class WorldModelModule { 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'); + // Atomic + verified: never promote a write we did not read back byte-for-byte. + // Measured 2026-10-23: the old block promoted /system/world-model.json even when every + // write returned false, and logged nothing — that is how a graph got replaced by a + // partial model. fs.fsWriteJSON() returns a boolean; ignoring it hides quota failures. + const payload = JSON.stringify(this._model); + const wrote = fs.writeFile('/system/world-model.tmp.json', payload); + if (!wrote) { + console.error('[WorldModel] Save failed: write returned false (storage quota?)'); + return; + } + const back = fs.readFile('/system/world-model.tmp.json'); + if (back !== payload) { + console.error('[WorldModel] Save failed: read-back mismatch (' + + (back ? back.length : 0) + '/' + payload.length + ' bytes) — keeping previous model'); + return; + } + const promoted = fs.writeFile('/system/world-model.json', back); + if (!promoted) { + console.error('[WorldModel] Save failed: promote returned false'); + return; } + fs.deleteFile('/system/world-model.tmp.json'); } catch (e) { console.error('[WorldModel] Save error:', e.message); } @@ -20009,6 +20035,8 @@ class DebugSupervisorModule { 'debug-health': 'debug-health — Check supervisor module health status', 'debug-sync': 'debug-sync [url] — Sync errors to backend API (or set sync URL)', 'debug-sync-url': 'debug-sync-url [url] — View or set the backend sync endpoint', + 'feedback': 'feedback <message> — Send feedback to the WebX11 maintenance agent', + 'feedback-inbox': 'feedback-inbox — Read replies from the maintenance agent (admin session)', }; } @@ -20021,6 +20049,8 @@ class DebugSupervisorModule { this.commands['debug-health'] = () => this._cmdHealth(); this.commands['debug-sync'] = (args) => this._cmdSync(args); this.commands['debug-sync-url'] = (args) => this._cmdSyncUrl(args); + this.commands['feedback'] = (args) => this._cmdFeedback(args); + this.commands['feedback-inbox'] = () => this._cmdFeedbackInbox(); } seed() { @@ -20029,6 +20059,9 @@ class DebugSupervisorModule { init() { this._loadDB(); + try { + this._syncedCounts = JSON.parse(localStorage.getItem('webx11_debug_synced') || '{}'); + } catch (_e) { this._syncedCounts = {}; } this._installInterceptors(); // Start periodic sync (every 5 minutes) this._syncInterval = setInterval(() => this._syncToBackend(), 300000); @@ -20297,6 +20330,12 @@ class DebugSupervisorModule { _maybeAutoFileInner(e) { try { if (localStorage.getItem('webx11_autoreport') === 'off') return; } catch (_e) {} + // Aborts/timeouts are the client's own cancellation, not application bugs, and the debug + // pipeline's own requests must never be filed as reports (self-referential loop). + // Measured 2026-10-21..23: 2 of 5 auto-filed reports were "user aborted a request". + if (/(user aborted|aborted without reason|signal timed out|AbortError|TimeoutError)/i.test(String(e.message || ''))) return; + if (/\/api\/debug\//.test(String(e.source || '')) || /\/api\/debug\//.test(String(e.message || ''))) return; + const today = new Date().toISOString().slice(0, 10); let state = { day: today, count: 0, hashes: [] }; try { @@ -20732,7 +20771,12 @@ class DebugSupervisorModule { async _syncToBackend() { if (!this._healthy) return; const db = this._errorDB.errors; - const keys = Object.keys(db); + const allKeys = Object.keys(db); + if (allKeys.length === 0) return; + // Only send hashes whose cumulative count changed since the last successful sync. + // The server counts deltas per (session, hash); re-sending unchanged rows is what + // multiplied one error into 46,133 occurrences before 2026-10-23. + const keys = allKeys.filter(k => (this._syncedCounts[k] || 0) !== db[k].count); if (keys.length === 0) return; const payload = { @@ -20767,12 +20811,14 @@ class DebugSupervisorModule { const result = await resp.json(); if (result.success) { const sent = keys.length; - const accepted = result.ingested || 0; + const accepted = result.accepted ?? result.ingested ?? 0; if (accepted < sent) { console.warn('[DebugSupervisor] Sync truncated: sent ' + sent + ' errors, server accepted ' + accepted + ' (max ' + (result.total_errors || '?') + ' total)'); } else { console.log('[DebugSupervisor] Synced ' + accepted + ' errors to backend'); } + keys.forEach(k => { this._syncedCounts[k] = db[k].count; }); + try { localStorage.setItem('webx11_debug_synced', JSON.stringify(this._syncedCounts)); } catch (_e) {} this._lastSyncOk = Date.now(); return true; } @@ -20785,6 +20831,49 @@ class DebugSupervisorModule { } } + _feedbackThread() { + let t = null; + try { t = localStorage.getItem('webx11_feedback_thread'); } catch (_e) {} + if (!t) { + t = 'thr-' + Math.random().toString(36).slice(2, 10); + try { localStorage.setItem('webx11_feedback_thread', t); } catch (_e) {} + } + return t; + } + + _cmdFeedback(args) { + const msg = (Array.isArray(args) ? args.join(' ') : String(args || '')).trim(); + if (!msg) { this.terminal.print('usage: feedback <message>', 'error'); return; } + const payload = { + title: msg.slice(0, 120), + body: msg.slice(0, 4000), + thread: this._feedbackThread(), + kind: 'maintenance', + diagnostics: { version: window.NEWOS4_VERSION || '?', url: location.href, auto: false }, + }; + this.terminal.print('📨 Sending feedback…', ''); + fetch('/spaces/newos4/api/debug/report.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }).then(r => r.json()).then(j => { + if (j && j.success) this.terminal.print('✅ Sent: ' + (j.id || 'ok') + (j.duplicate ? ' (merged)' : ''), 'success'); + else this.terminal.print('❌ ' + ((j && j.error) || 'send failed'), 'error'); + }).catch(e => this.terminal.print('❌ ' + e.message, 'error')); + } + + _cmdFeedbackInbox() { + this.terminal.print('📬 Fetching replies…', ''); + fetch('/spaces/newos4/api/debug/agent-replies.php', { credentials: 'same-origin' }) + .then(r => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status + ' (admin session required)'))) + .then(j => { + const list = (j && j.replies) || []; + if (!list.length) { this.terminal.print('(no replies yet)', ''); return; } + list.slice(-10).forEach(r => this.terminal.print('🤖 ' + (r.created_at || '') + ' ' + r.body, '')); + }) + .catch(e => this.terminal.print('❌ ' + e.message, 'error')); + } + _cmdSync(args) { const url = (args && args[0]) ? args[0] : null; if (url) { diff --git a/modules/core/module-debug-supervisor.js b/modules/core/module-debug-supervisor.js index 959d162..485f4e2 100644 --- a/modules/core/module-debug-supervisor.js +++ b/modules/core/module-debug-supervisor.js @@ -41,6 +41,8 @@ class DebugSupervisorModule { 'debug-health': 'debug-health — Check supervisor module health status', 'debug-sync': 'debug-sync [url] — Sync errors to backend API (or set sync URL)', 'debug-sync-url': 'debug-sync-url [url] — View or set the backend sync endpoint', + 'feedback': 'feedback <message> — Send feedback to the WebX11 maintenance agent', + 'feedback-inbox': 'feedback-inbox — Read replies from the maintenance agent (admin session)', }; } @@ -53,6 +55,8 @@ class DebugSupervisorModule { this.commands['debug-health'] = () => this._cmdHealth(); this.commands['debug-sync'] = (args) => this._cmdSync(args); this.commands['debug-sync-url'] = (args) => this._cmdSyncUrl(args); + this.commands['feedback'] = (args) => this._cmdFeedback(args); + this.commands['feedback-inbox'] = () => this._cmdFeedbackInbox(); } seed() { @@ -61,6 +65,9 @@ class DebugSupervisorModule { init() { this._loadDB(); + try { + this._syncedCounts = JSON.parse(localStorage.getItem('webx11_debug_synced') || '{}'); + } catch (_e) { this._syncedCounts = {}; } this._installInterceptors(); // Start periodic sync (every 5 minutes) this._syncInterval = setInterval(() => this._syncToBackend(), 300000); @@ -329,6 +336,12 @@ class DebugSupervisorModule { _maybeAutoFileInner(e) { try { if (localStorage.getItem('webx11_autoreport') === 'off') return; } catch (_e) {} + // Aborts/timeouts are the client's own cancellation, not application bugs, and the debug + // pipeline's own requests must never be filed as reports (self-referential loop). + // Measured 2026-10-21..23: 2 of 5 auto-filed reports were "user aborted a request". + if (/(user aborted|aborted without reason|signal timed out|AbortError|TimeoutError)/i.test(String(e.message || ''))) return; + if (/\/api\/debug\//.test(String(e.source || '')) || /\/api\/debug\//.test(String(e.message || ''))) return; + const today = new Date().toISOString().slice(0, 10); let state = { day: today, count: 0, hashes: [] }; try { @@ -764,7 +777,12 @@ class DebugSupervisorModule { async _syncToBackend() { if (!this._healthy) return; const db = this._errorDB.errors; - const keys = Object.keys(db); + const allKeys = Object.keys(db); + if (allKeys.length === 0) return; + // Only send hashes whose cumulative count changed since the last successful sync. + // The server counts deltas per (session, hash); re-sending unchanged rows is what + // multiplied one error into 46,133 occurrences before 2026-10-23. + const keys = allKeys.filter(k => (this._syncedCounts[k] || 0) !== db[k].count); if (keys.length === 0) return; const payload = { @@ -799,12 +817,14 @@ class DebugSupervisorModule { const result = await resp.json(); if (result.success) { const sent = keys.length; - const accepted = result.ingested || 0; + const accepted = result.accepted ?? result.ingested ?? 0; if (accepted < sent) { console.warn('[DebugSupervisor] Sync truncated: sent ' + sent + ' errors, server accepted ' + accepted + ' (max ' + (result.total_errors || '?') + ' total)'); } else { console.log('[DebugSupervisor] Synced ' + accepted + ' errors to backend'); } + keys.forEach(k => { this._syncedCounts[k] = db[k].count; }); + try { localStorage.setItem('webx11_debug_synced', JSON.stringify(this._syncedCounts)); } catch (_e) {} this._lastSyncOk = Date.now(); return true; } @@ -817,6 +837,49 @@ class DebugSupervisorModule { } } + _feedbackThread() { + let t = null; + try { t = localStorage.getItem('webx11_feedback_thread'); } catch (_e) {} + if (!t) { + t = 'thr-' + Math.random().toString(36).slice(2, 10); + try { localStorage.setItem('webx11_feedback_thread', t); } catch (_e) {} + } + return t; + } + + _cmdFeedback(args) { + const msg = (Array.isArray(args) ? args.join(' ') : String(args || '')).trim(); + if (!msg) { this.terminal.print('usage: feedback <message>', 'error'); return; } + const payload = { + title: msg.slice(0, 120), + body: msg.slice(0, 4000), + thread: this._feedbackThread(), + kind: 'maintenance', + diagnostics: { version: window.NEWOS4_VERSION || '?', url: location.href, auto: false }, + }; + this.terminal.print('📨 Sending feedback…', ''); + fetch('/spaces/newos4/api/debug/report.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }).then(r => r.json()).then(j => { + if (j && j.success) this.terminal.print('✅ Sent: ' + (j.id || 'ok') + (j.duplicate ? ' (merged)' : ''), 'success'); + else this.terminal.print('❌ ' + ((j && j.error) || 'send failed'), 'error'); + }).catch(e => this.terminal.print('❌ ' + e.message, 'error')); + } + + _cmdFeedbackInbox() { + this.terminal.print('📬 Fetching replies…', ''); + fetch('/spaces/newos4/api/debug/agent-replies.php', { credentials: 'same-origin' }) + .then(r => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status + ' (admin session required)'))) + .then(j => { + const list = (j && j.replies) || []; + if (!list.length) { this.terminal.print('(no replies yet)', ''); return; } + list.slice(-10).forEach(r => this.terminal.print('🤖 ' + (r.created_at || '') + ' ' + r.body, '')); + }) + .catch(e => this.terminal.print('❌ ' + e.message, 'error')); + } + _cmdSync(args) { const url = (args && args[0]) ? args[0] : null; if (url) { diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 46aad17..f22576a 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.51'; + const LATEST_VER = 'v4.6.53'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { |