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:
Gmer4Lfe
2026-08-13 15:13:32 -04:00
parent 17216a354d
commit be6bb3ffc5
2 changed files with 126 additions and 15 deletions
+77 -7
View File
@@ -191,6 +191,42 @@ function vv_ai_mem_append(string $text): array {
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 {
$out = [];
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,
// because none of those are visible from the one that is about to lose.
function vv_ai_mem_action(string $id, string $act): array {
if (!preg_match('/^[0-9a-f]{12}$/', $id)) return ['ok' => false, 'error' => 'bad id'];
if ($act !== 'accept' && $act !== 'dismiss') return ['ok' => false, 'error' => 'unknown action'];
// open → accept | dismiss the original decision
// 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";
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.
$d = json_decode((string)@file_get_contents($f), true);
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.
return ['ok' => false, 'error' => 'already ' . ($d['state'] ?? 'closed')];
// A tab left open overnight must not act on a choice the store has already moved past.
$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']);
if (!$r['ok']) return $r;
$d['state'] = 'accepted';
$d['accepted'] = time();
} else {
unset($d['closed']);
} elseif ($act === 'dismiss') {
$d['state'] = 'dismissed';
$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)) {