Let a kept memory be taken back
Accepting put a line in every future prompt with no way out but editing the file by hand, and a fact that is true today stops being true.
This commit is contained in:
@@ -191,6 +191,42 @@ function vv_ai_mem_append(string $text): array {
|
|||||||
return vv_ai_memory_write($next, 'learned');
|
return vv_ai_memory_write($next, 'learned');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Takes one line back out of the learned slot. The counterpart to vv_ai_mem_append(), and the
|
||||||
|
// reason a decided proposal can be changed at all: without this, "accepted" was a one-way door
|
||||||
|
// and the only way out was editing mem_learned.md by hand. An accepted line joins every future
|
||||||
|
// prompt, and a fact that is true today — HOST2 is offline — becomes actively wrong the day it
|
||||||
|
// stops being true, with nothing to expire it.
|
||||||
|
//
|
||||||
|
// Matched on the line this file wrote: "- <text> (<date>)". A line the operator has since edited
|
||||||
|
// by hand will not match, and that is reported rather than swallowed — silently succeeding while
|
||||||
|
// the text stays in every prompt is the one outcome worse than failing.
|
||||||
|
function vv_ai_mem_remove(string $text): array {
|
||||||
|
$p = vv_ai_memory_path('learned');
|
||||||
|
if (!file_exists($p)) return ['ok' => false, 'error' => 'learned memory file does not exist'];
|
||||||
|
|
||||||
|
$cur = (string) @file_get_contents($p);
|
||||||
|
$lines = preg_split('/\R/', $cur);
|
||||||
|
$want = vv_ai_mem_norm($text);
|
||||||
|
$out = [];
|
||||||
|
$hit = false;
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
// Compare the line's own text, stripped of the bullet and the trailing date stamp, so a
|
||||||
|
// change in date format does not strand the entry.
|
||||||
|
$bare = preg_replace('/^\s*-\s*/', '', $line);
|
||||||
|
$bare = preg_replace('/\s*\(\d{4}-\d{2}-\d{2}\)\s*$/', '', $bare);
|
||||||
|
if (!$hit && $bare !== '' && vv_ai_mem_norm($bare) === $want) { $hit = true; continue; }
|
||||||
|
$out[] = $line;
|
||||||
|
}
|
||||||
|
if (!$hit) return ['ok' => false, 'error' => 'that line is not in learned memory — it may have been edited by hand'];
|
||||||
|
|
||||||
|
$next = rtrim(implode("\n", $out)) . "\n";
|
||||||
|
// Only the header left means nothing is being remembered; write it back empty rather than
|
||||||
|
// leaving a file that looks populated.
|
||||||
|
if (trim(preg_replace('/^#.*$/m', '', $next)) === '') $next = '';
|
||||||
|
return vv_ai_memory_write($next, 'learned');
|
||||||
|
}
|
||||||
|
|
||||||
function vv_ai_mem_list(string $state = ''): array {
|
function vv_ai_mem_list(string $state = ''): array {
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach (glob(vv_ai_mem_dir() . '/*.json') ?: [] as $f) {
|
foreach (glob(vv_ai_mem_dir() . '/*.json') ?: [] as $f) {
|
||||||
@@ -222,8 +258,18 @@ function vv_ai_mem_write_row(string $f, array $row): bool {
|
|||||||
// against a second tab, a double submit or an impatient reload; that is exactly when it matters,
|
// against a second tab, a double submit or an impatient reload; that is exactly when it matters,
|
||||||
// because none of those are visible from the one that is about to lose.
|
// because none of those are visible from the one that is about to lose.
|
||||||
function vv_ai_mem_action(string $id, string $act): array {
|
function vv_ai_mem_action(string $id, string $act): array {
|
||||||
if (!preg_match('/^[0-9a-f]{12}$/', $id)) return ['ok' => false, 'error' => 'bad id'];
|
// open → accept | dismiss the original decision
|
||||||
if ($act !== 'accept' && $act !== 'dismiss') return ['ok' => false, 'error' => 'unknown action'];
|
// accepted → retract takes the line back out of every future prompt
|
||||||
|
// dismissed → keep changes your mind, puts it back
|
||||||
|
// decided → forget drops the record entirely
|
||||||
|
//
|
||||||
|
// A decision used to be final, which made "accepted" a one-way door into the prompt. The
|
||||||
|
// transitions below are the way back out; each is checked against the state it is legal from,
|
||||||
|
// so a stale tab cannot retract something that was already forgotten.
|
||||||
|
$legal = ['accept' => 'open', 'dismiss' => 'open',
|
||||||
|
'retract' => 'accepted', 'keep' => 'dismissed', 'forget' => '*'];
|
||||||
|
if (!preg_match('/^[0-9a-f]{12}$/', $id)) return ['ok' => false, 'error' => 'bad id'];
|
||||||
|
if (!isset($legal[$act])) return ['ok' => false, 'error' => 'unknown action'];
|
||||||
|
|
||||||
$f = vv_ai_mem_dir() . "/$id.json";
|
$f = vv_ai_mem_dir() . "/$id.json";
|
||||||
if (!file_exists($f)) return ['ok' => false, 'error' => 'no such proposal'];
|
if (!file_exists($f)) return ['ok' => false, 'error' => 'no such proposal'];
|
||||||
@@ -239,19 +285,43 @@ function vv_ai_mem_action(string $id, string $act): array {
|
|||||||
// evidence of anything — the decision may already have been made in another tab.
|
// evidence of anything — the decision may already have been made in another tab.
|
||||||
$d = json_decode((string)@file_get_contents($f), true);
|
$d = json_decode((string)@file_get_contents($f), true);
|
||||||
if (!is_array($d)) return ['ok' => false, 'error' => 'unreadable proposal'];
|
if (!is_array($d)) return ['ok' => false, 'error' => 'unreadable proposal'];
|
||||||
if (($d['state'] ?? '') !== 'open') {
|
|
||||||
// A tab left open overnight must not act on a choice the store has already moved past.
|
// A tab left open overnight must not act on a choice the store has already moved past.
|
||||||
return ['ok' => false, 'error' => 'already ' . ($d['state'] ?? 'closed')];
|
$state = (string) ($d['state'] ?? '');
|
||||||
|
if ($legal[$act] !== '*' && $state !== $legal[$act]) {
|
||||||
|
return ['ok' => false, 'error' => 'this is ' . ($state ?: 'in no state')
|
||||||
|
. ', so it cannot be ' . $act . 'ed'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($act === 'accept') {
|
if ($act === 'accept' || $act === 'keep') {
|
||||||
$r = vv_ai_mem_append((string)$d['text']);
|
$r = vv_ai_mem_append((string)$d['text']);
|
||||||
if (!$r['ok']) return $r;
|
if (!$r['ok']) return $r;
|
||||||
$d['state'] = 'accepted';
|
$d['state'] = 'accepted';
|
||||||
$d['accepted'] = time();
|
$d['accepted'] = time();
|
||||||
} else {
|
unset($d['closed']);
|
||||||
|
} elseif ($act === 'dismiss') {
|
||||||
$d['state'] = 'dismissed';
|
$d['state'] = 'dismissed';
|
||||||
$d['closed'] = time();
|
$d['closed'] = time();
|
||||||
|
} elseif ($act === 'retract') {
|
||||||
|
// The line leaves memory first. If that fails the proposal keeps saying "accepted",
|
||||||
|
// which is the truth — the text is still in every prompt.
|
||||||
|
$r = vv_ai_mem_remove((string)$d['text']);
|
||||||
|
if (!$r['ok']) return $r;
|
||||||
|
$d['state'] = 'dismissed';
|
||||||
|
$d['closed'] = time();
|
||||||
|
unset($d['accepted']);
|
||||||
|
} elseif ($act === 'forget') {
|
||||||
|
// Retract first when it is live, or the record vanishes while its text stays in the
|
||||||
|
// prompt with nothing left pointing at it.
|
||||||
|
if ($state === 'accepted') {
|
||||||
|
$r = vv_ai_mem_remove((string)$d['text']);
|
||||||
|
if (!$r['ok']) return $r;
|
||||||
|
}
|
||||||
|
// Dropping the record also drops what stops it being proposed again — dedup checks
|
||||||
|
// every past proposal including dismissed ones. That is the point of forgetting
|
||||||
|
// rather than dismissing, and it is why the button says so.
|
||||||
|
if (!@unlink($f)) return ['ok' => false, 'error' => 'could not remove the proposal'];
|
||||||
|
return ['ok' => true, 'state' => 'forgotten'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vv_ai_mem_write_row($f, $d)) {
|
if (!vv_ai_mem_write_row($f, $d)) {
|
||||||
|
|||||||
@@ -176,6 +176,13 @@ if (is_dir('/var/log/varaverk')) {
|
|||||||
.vv-ai-src-tag.repair { color:#d8a15a; border-color:#4a3a1e; background:#1a1408; }
|
.vv-ai-src-tag.repair { color:#d8a15a; border-color:#4a3a1e; background:#1a1408; }
|
||||||
.vv-ai-src-tag.memory { color:#7d9be8; border-color:#26324a; background:#0d1220; }
|
.vv-ai-src-tag.memory { color:#7d9be8; border-color:#26324a; background:#0d1220; }
|
||||||
|
|
||||||
|
/* What was decided, in a word. A decided row is greyed, which says it is settled but not which
|
||||||
|
way — and "kept" is the one that matters, because it means this text is in every prompt now. */
|
||||||
|
.vv-ai-mem-state { font-size:9px; letter-spacing:.06em; text-transform:uppercase;
|
||||||
|
border-radius:3px; padding:1px 6px; border:1px solid; flex-shrink:0; }
|
||||||
|
.vv-ai-mem-state.accepted { color:#6fcf97; border-color:#264a26; background:#0c1a0c; }
|
||||||
|
.vv-ai-mem-state.dismissed { color:#6a6a6a; border-color:#2a2a2a; background:#131313; }
|
||||||
|
|
||||||
/* Filter pills, loosely the shape the Monitor scripts card uses: a count you can click. All is an
|
/* Filter pills, loosely the shape the Monitor scripts card uses: a count you can click. All is an
|
||||||
explicit pill rather than "click the active one again to clear" — deselecting to get everything
|
explicit pill rather than "click the active one again to clear" — deselecting to get everything
|
||||||
back is a gesture you have to already know. A source with nothing in it still shows its pill, so
|
back is a gesture you have to already know. A source with nothing in it still shows its pill, so
|
||||||
@@ -714,7 +721,15 @@ vv_ai_chat_markup('vv-ai', [
|
|||||||
return act === 'fix' ? 'Confirm write'
|
return act === 'fix' ? 'Confirm write'
|
||||||
: act === 'dismiss' ? 'Confirm — permanent' : null;
|
: act === 'dismiss' ? 'Confirm — permanent' : null;
|
||||||
}
|
}
|
||||||
return act === 'accept' ? 'Confirm keep' : null;
|
// Keeping is armed because it is the only route by which model-written text reaches a future
|
||||||
|
// prompt. Forgetting is armed because it drops the record that stops the same line being
|
||||||
|
// proposed again, and there is no undo for a provenance you no longer have.
|
||||||
|
//
|
||||||
|
// Retract and dismiss are not: both only ever remove something, and both are one click from
|
||||||
|
// being put back.
|
||||||
|
if (act === 'accept' || act === 'keep') return 'Confirm keep';
|
||||||
|
if (act === 'forget') return 'Confirm forget';
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
let fndArmTimer = null;
|
let fndArmTimer = null;
|
||||||
|
|
||||||
@@ -875,22 +890,42 @@ vv_ai_chat_markup('vv-ai', [
|
|||||||
|
|
||||||
function qMemoryHtml(row) {
|
function qMemoryHtml(row) {
|
||||||
const r = row.raw;
|
const r = row.raw;
|
||||||
const meta = [r.id, ago(r.created), r.profile || null,
|
const meta = [r.id, ago(r.created), r.profile || null].filter(Boolean).join(' · ');
|
||||||
row.closed ? r.state + (r.auto ? ' (auto)' : '') : null].filter(Boolean).join(' · ');
|
// Said as a word rather than left to be read off the greying, because "kept" is the state
|
||||||
|
// that matters — it means this text is in every prompt right now.
|
||||||
|
const stateWord = r.state === 'accepted' ? (r.auto ? 'kept automatically' : 'kept')
|
||||||
|
: r.state === 'dismissed' ? 'dismissed' : '';
|
||||||
|
const badge = row.closed
|
||||||
|
? `<span class="vv-ai-mem-state ${esc(r.state)}">${esc(stateWord)}</span>` : '';
|
||||||
// The question is the provenance. A line read three weeks from now is judged by what was
|
// 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.
|
// being asked when the model decided it was worth keeping.
|
||||||
const asked = r.asked ? `<div class="vv-ai-fnd-n">asked: ${esc(r.asked)}</div>` : '';
|
const asked = r.asked ? `<div class="vv-ai-fnd-n">asked: ${esc(r.asked)}</div>` : '';
|
||||||
// No severity class: a proposal is not a fault, and colouring it like one would make a card
|
// No severity class: a proposal is not a fault, and colouring it like one would make a card
|
||||||
// of ordinary suggestions read as a card of problems.
|
// of ordinary suggestions read as a card of problems.
|
||||||
const acts = row.closed ? '' :
|
//
|
||||||
`<button class="vv-ai-btn" data-mem-act="accept" data-id="${esc(r.id)}" `
|
// A decision is not final. An accepted line joins every future prompt and nothing expires it,
|
||||||
+ `title="Add this to learned memory">Keep</button>`
|
// so a fact that is true today — HOST2 is offline — needs a way back out on the day it stops
|
||||||
+ `<button class="vv-ai-btn ghost" data-mem-act="dismiss" data-id="${esc(r.id)}" `
|
// being true. Retract is that way; Keep is the same door in the other direction.
|
||||||
+ `title="Never propose this again">Dismiss</button>`;
|
const B = (act, cls, label, tip) =>
|
||||||
|
`<button class="vv-ai-btn${cls}" data-mem-act="${act}" data-id="${esc(r.id)}" `
|
||||||
|
+ `title="${esc(tip)}">${esc(label)}</button>`;
|
||||||
|
|
||||||
|
let acts;
|
||||||
|
if (!row.closed) {
|
||||||
|
acts = B('accept', '', 'Keep', 'Add this to learned memory')
|
||||||
|
+ B('dismiss', ' ghost', 'Dismiss', 'Do not keep it, and do not propose it again');
|
||||||
|
} else if (r.state === 'accepted') {
|
||||||
|
acts = B('retract', '', 'Retract', 'Take this back out of every future prompt')
|
||||||
|
+ B('forget', ' ghost', 'Forget', 'Remove it and stop tracking it — it may be proposed again');
|
||||||
|
} else {
|
||||||
|
acts = B('keep', '', 'Keep', 'Changed your mind — add it to learned memory after all')
|
||||||
|
+ B('forget', ' ghost', 'Forget', 'Stop tracking it — it may be proposed again');
|
||||||
|
}
|
||||||
|
|
||||||
return `<div class="vv-ai-fnd${row.closed ? ' closed' : ''}">
|
return `<div class="vv-ai-fnd${row.closed ? ' closed' : ''}">
|
||||||
<div class="vv-ai-fnd-h">
|
<div class="vv-ai-fnd-h">
|
||||||
<span class="vv-ai-src-tag memory">memory</span>
|
<span class="vv-ai-src-tag memory">memory</span>
|
||||||
|
${badge}
|
||||||
<span class="vv-ai-fnd-m">${esc(meta)}</span>
|
<span class="vv-ai-fnd-m">${esc(meta)}</span>
|
||||||
</div>
|
</div>
|
||||||
<pre class="vv-ai-fnd-e">${esc(r.text)}</pre>
|
<pre class="vv-ai-fnd-e">${esc(r.text)}</pre>
|
||||||
@@ -973,6 +1008,12 @@ vv_ai_chat_markup('vv-ai', [
|
|||||||
return 'Stays closed even when it is seen again. '
|
return 'Stays closed even when it is seen again. '
|
||||||
+ 'Use “I know” to be told if it changes. Click again to confirm.';
|
+ 'Use “I know” to be told if it changes. Click again to confirm.';
|
||||||
}
|
}
|
||||||
|
if (act === 'forget') {
|
||||||
|
return 'Drops the record, so nothing stops this being proposed again — dismissed rows are '
|
||||||
|
+ 'what suppress a repeat.'
|
||||||
|
+ (row.raw.state === 'accepted' ? ' It is removed from memory first.' : '')
|
||||||
|
+ ' Click again to confirm.';
|
||||||
|
}
|
||||||
return 'This text joins every future prompt. Click again to confirm.';
|
return 'This text joins every future prompt. Click again to confirm.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user