aboutsummaryrefslogtreecommitdiff
path: root/modules/core/core-bundle.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/core-bundle.js')
-rw-r--r--modules/core/core-bundle.js107
1 files changed, 98 insertions, 9 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) {