From 2100286dbb2b4f026e97be51043d104e2f361923 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Mon, 10 Aug 2026 22:06:28 -0400 Subject: [PATCH] Show proposed memory beside findings, so keeping one is a decision and not a default --- Plugin/unraid/pages/ai.php | 124 +++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/Plugin/unraid/pages/ai.php b/Plugin/unraid/pages/ai.php index f391bdf..863917c 100644 --- a/Plugin/unraid/pages/ai.php +++ b/Plugin/unraid/pages/ai.php @@ -253,6 +253,22 @@ if (is_dir('/var/log/varaverk')) { + +
+
+
+ Proposed memory + + +
+
+
+
+ @@ -774,6 +790,113 @@ vv_ai_chat_markup('vv-ai', [ loadFindings(); }); + // ── Proposed memory ─────────────────────────────────────────────────────── + // Accepting is the only route by which text the model wrote reaches a future prompt, so it is + // the one control here that has to be deliberate. Same two-press arming the findings card uses + // for a conf write, for the same reason. + let memAll = false, memRows = []; + + function loadMemProposals() { + fetch(API + '?action=mem_proposals').then(r => r.json()) + .then(d => { if (d.ok) renderMemProposals(d); }) + .catch(() => {}); + } + + function renderMemProposals(d) { + memRows = memAll ? (d.recent || []) : (d.open || []); + const L = d.learned || {}; + + // The gate is the news when the list is empty. "Nothing proposed" and "nothing is proposing" + // look identical otherwise, and they are opposite states. + const gate = []; + gate.push(d.enabled ? (d.auto ? 'learning on · auto-accept on' + : 'learning on · proposals held for you') + : 'learning off — nothing is proposing'); + if (L.max) gate.push('learned ' + (L.chars || 0) + '/' + L.max + ' chars'); + $('vv-ai-mem-gate').innerHTML = (d.enabled && !d.auto) + ? esc(gate.join(' · ')) + : `${esc(gate.join(' · '))}`; + + const openN = (d.open || []).length; + $('vv-ai-mem-sum').innerHTML = openN + ? `${openN} to review` + : `✓ none waiting`; + + if (!memRows.length) { + $('vv-ai-mem-list').innerHTML = memAll + ? '
nothing proposed yet
' + : '
nothing waiting on you
'; + return; + } + + $('vv-ai-mem-list').innerHTML = memRows.map(r => { + const decided = r.state !== 'open'; + const meta = [r.id, ago(r.created), r.profile || null, + decided ? r.state + (r.auto ? ' (auto)' : '') : null].filter(Boolean).join(' · '); + // The question is the provenance. A line read three weeks from now is judged by what was + // being asked when the model decided it was worth keeping. + const asked = r.asked ? `
asked: ${esc(r.asked)}
` : ''; + const acts = decided ? '' : + `` + + ``; + + return `
+
${esc(meta)}
+
${esc(r.text)}
+ ${asked} +
${acts}
+
`; + }).join(''); + } + + $('vv-ai-mem-list').addEventListener('click', e => { + const btn = e.target.closest('[data-mem-act]'); + if (!btn) return; + + const act = btn.dataset.memAct, id = btn.dataset.memId; + const msg = btn.parentNode.querySelector('[data-msg]'); + + // Keep arms first. Dismiss does not: it writes nothing to any prompt and is undone by the + // model proposing something similar again, so a confirm there would be ceremony. + if (act === 'accept' && btn.dataset.armed !== '1') { + btn.dataset.armed = '1'; + btn.dataset.label = btn.dataset.label || btn.textContent; + btn.textContent = 'Confirm keep'; + btn.classList.add('armed'); + if (msg) msg.textContent = 'this text joins every future prompt'; + return; + } + + const btns = Array.from(btn.parentNode.querySelectorAll('button')); + btns.forEach(b => b.disabled = true); + if (msg) msg.textContent = 'working…'; + + // Reload on success only — same rule as findings. A refused keep (learned memory full) is + // the case that must not vanish, and a reload would repaint the row and take the reason away. + const failed = text => { + btn.dataset.armed = ''; + if (btn.dataset.label) btn.textContent = btn.dataset.label; + btn.classList.remove('armed'); + btns.forEach(b => b.disabled = false); + if (msg) msg.innerHTML = `${esc(text)}`; + }; + + fetch(API, { method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + body: new URLSearchParams({ action: 'mem_proposal_action', id, act }) }) + .then(r => r.json()) + .then(d => { if (d.ok) loadMemProposals(); else failed(d.error || 'failed'); }) + .catch(e => failed(String(e))); + }); + + $('vv-ai-mem-all').addEventListener('click', () => { + memAll = !memAll; + $('vv-ai-mem-all').textContent = memAll ? 'Waiting only' : 'Show decided'; + loadMemProposals(); + }); + // ── Token accounting ──────────────────────────────────────────────────── // Fetched on load and after each completed turn, never on the 30s banner tick: the totals // only move when a turn finishes, and the page is the thing that knows when that was. @@ -969,6 +1092,7 @@ vv_ai_chat_markup('vv-ai', [ loadTokens(); loadBugs(); loadFindings(); + loadMemProposals(); chatList.reload(); })();