Show proposed memory beside findings, so keeping one is a decision and not a default

This commit is contained in:
Gmer4Lfe
2026-08-10 22:06:28 -04:00
parent 29e0daf80d
commit 2100286dbb
+124
View File
@@ -253,6 +253,22 @@ if (is_dir('/var/log/varaverk')) {
</div> </div>
</div> </div>
<!-- Proposed memory. Sits beside findings deliberately: both are the assistant asking for a
decision rather than reporting a fact, and both are worthless if the answer is a click the
operator never sees. Shown even when empty for the same reason the findings card is — the
gate line is the news when there is nothing else. -->
<div class="vv-ai-tok" id="vv-ai-mem-wrap">
<div class="vv-ai-diag-col" style="grid-column:1/-1">
<div class="vv-ai-diag-h">
<span id="vv-ai-mem-sum"></span> Proposed memory
<span class="vv-ai-set-sum" style="margin-left:auto" id="vv-ai-mem-gate"></span>
<button class="vv-ai-btn ghost" id="vv-ai-mem-all" type="button"
style="padding:2px 9px;font-size:10px;text-transform:none;letter-spacing:0">Show decided</button>
</div>
<div id="vv-ai-mem-list"></div>
</div>
</div>
<!-- Bug reports the scheduler's troubleshooter filed. Hidden entirely when there are none: <!-- Bug reports the scheduler's troubleshooter filed. Hidden entirely when there are none:
an empty card here would be a permanent reminder of nothing, and this row already an empty card here would be a permanent reminder of nothing, and this row already
competes for the space above the transcript. --> competes for the space above the transcript. -->
@@ -774,6 +790,113 @@ vv_ai_chat_markup('vv-ai', [
loadFindings(); 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(' · '))
: `<span class="vv-ai-warn">${esc(gate.join(' · '))}</span>`;
const openN = (d.open || []).length;
$('vv-ai-mem-sum').innerHTML = openN
? `<span class="vv-ai-warn">${openN} to review</span>`
: `<span class="vv-ai-ok">✓ none waiting</span>`;
if (!memRows.length) {
$('vv-ai-mem-list').innerHTML = memAll
? '<div class="vv-ai-none">nothing proposed yet</div>'
: '<div class="vv-ai-none">nothing waiting on you</div>';
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 ? `<div class="vv-ai-fnd-r">asked: ${esc(r.asked)}</div>` : '';
const acts = decided ? '' :
`<button class="vv-ai-btn" data-mem-act="accept" data-mem-id="${esc(r.id)}" `
+ `title="Add this to learned memory">Keep</button>`
+ `<button class="vv-ai-btn ghost" data-mem-act="dismiss" data-mem-id="${esc(r.id)}" `
+ `title="Never propose this again">Dismiss</button>`;
return `<div class="vv-ai-fnd${decided ? ' closed' : ''}">
<div class="vv-ai-fnd-h"><span class="vv-ai-fnd-m">${esc(meta)}</span></div>
<pre class="vv-ai-fnd-e">${esc(r.text)}</pre>
${asked}
<div class="vv-ai-fnd-a">${acts}<span data-msg></span></div>
</div>`;
}).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 = `<span class="vv-ai-bad">${esc(text)}</span>`;
};
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 ──────────────────────────────────────────────────── // ── Token accounting ────────────────────────────────────────────────────
// Fetched on load and after each completed turn, never on the 30s banner tick: the totals // 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. // 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(); loadTokens();
loadBugs(); loadBugs();
loadFindings(); loadFindings();
loadMemProposals();
chatList.reload(); chatList.reload();
})(); })();
</script> </script>