Let each control actually govern the thing it names

Auto Scroll, the learning gate, the fold and the flash label each described a
behaviour they did not enforce.
This commit is contained in:
Gmer4Lfe
2026-08-11 17:25:51 -04:00
parent aa3d92360e
commit e3ec54f213
4 changed files with 162 additions and 39 deletions
+55 -20
View File
@@ -139,7 +139,9 @@ function vv_ai_mem_propose(string $text, array $meta = []): array {
if (!$r['ok']) $row['error'] = $r['error'];
}
@file_put_contents(vv_ai_mem_dir() . "/$id.json", json_encode($row, JSON_PRETTY_PRINT));
if (!vv_ai_mem_write_row(vv_ai_mem_dir() . "/$id.json", $row)) {
return ['ok' => false, 'error' => 'could not file the proposal'];
}
return ['ok' => true, 'id' => $id, 'state' => $row['state']];
}
@@ -178,30 +180,63 @@ function vv_ai_mem_list(string $state = ''): array {
return $out;
}
// Temp and rename, so a concurrent list() never decodes half a record — a torn read here is
// indistinguishable from a corrupt proposal and the row silently vanishes from the card.
function vv_ai_mem_write_row(string $f, array $row): bool {
$tmp = $f . '.tmp';
if (@file_put_contents($tmp, json_encode($row, JSON_PRETTY_PRINT)) === false) {
@unlink($tmp);
return false;
}
if (!@rename($tmp, $f)) { @unlink($tmp); return false; }
return true;
}
// One lock for every decision, not one per proposal. Accept is a read-modify-write across two
// files — this proposal and the learned slot — and two of them interleaving is how the same line
// lands in memory twice, or how one of two accepted lines is lost to a temp-and-rename that began
// before the other finished. The card arms and disables the button, so this only has to hold
// 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 (!preg_match('/^[0-9a-f]{12}$/', $id)) return ['ok' => false, 'error' => 'bad id'];
if ($act !== 'accept' && $act !== 'dismiss') return ['ok' => false, 'error' => 'unknown action'];
$f = vv_ai_mem_dir() . "/$id.json";
if (!file_exists($f)) return ['ok' => false, 'error' => 'no such proposal'];
$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')];
$lock = @fopen(vv_ai_mem_dir() . '/.lock', 'c');
if ($lock === false || !flock($lock, LOCK_EX)) {
if ($lock !== false) fclose($lock);
return ['ok' => false, 'error' => 'could not lock the proposal store'];
}
if ($act === 'accept') {
$r = vv_ai_mem_append((string)$d['text']);
if (!$r['ok']) return $r;
$d['state'] = 'accepted';
$d['accepted'] = time();
} elseif ($act === 'dismiss') {
$d['state'] = 'dismissed';
$d['closed'] = time();
} else {
return ['ok' => false, 'error' => 'unknown action'];
}
try {
// Re-read under the lock. Whatever the card was showing when it was clicked is not
// 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')];
}
@file_put_contents($f, json_encode($d, JSON_PRETTY_PRINT));
return ['ok' => true, 'state' => $d['state']];
if ($act === 'accept') {
$r = vv_ai_mem_append((string)$d['text']);
if (!$r['ok']) return $r;
$d['state'] = 'accepted';
$d['accepted'] = time();
} else {
$d['state'] = 'dismissed';
$d['closed'] = time();
}
if (!vv_ai_mem_write_row($f, $d)) {
return ['ok' => false, 'error' => 'could not write the proposal'];
}
return ['ok' => true, 'state' => $d['state']];
} finally {
flock($lock, LOCK_UN);
fclose($lock);
}
}