Split memory into assisted and learned, and deny learned the precedence assisted has

This commit is contained in:
Gmer4Lfe
2026-08-10 21:50:35 -04:00
parent 775bdce6e1
commit 57f3bf2807
3 changed files with 155 additions and 15 deletions
+110 -10
View File
@@ -602,12 +602,63 @@ function vv_ai_is_definitional(string $q): bool {
// mechanism for "things I was told to remember". Living under DATA_DIR keeps it out of the
// repository and therefore out of the index by construction.
function vv_ai_memory_path(): string {
$cfg = vv_ai_config();
// Two memory slots, and the distinction between them is the whole point.
//
// assisted — written by the operator. Authoritative: the prompt tells the model to prefer it
// over retrieved passages, because a human asserting a fact about their own machine
// outranks a doc that may be stale.
// learned — proposed by the assistant and accepted by the operator. A hint only. Retrieval
// overrules it, and it is trimmed first when the budget bites. A model that can
// write memory the prompt ranks above the source code will restate its own mistakes
// forever, so it is deliberately given the weaker seat.
//
// Kept as two named slots rather than an arbitrary list: two is what the split is for, and a
// general N-file scheme would be machinery serving nobody.
function vv_ai_memory_path(string $kind = 'assisted'): string {
$vars = vv_conf_vars();
$p = trim($vars['AI_MEMORY_FILE'] ?? '');
$key = $kind === 'learned' ? 'AI_MEMORY_LEARNED_FILE' : 'AI_MEMORY_ASSISTED_FILE';
$p = trim($vars[$key] ?? '');
// Back-compat. Installs upgraded from the single-file era have AI_MEMORY_FILE set and a
// populated ai_memory.md; losing that silently would drop everything the operator had
// written. The legacy path wins only while the new one does not exist.
if ($kind === 'assisted') {
$legacy = trim($vars['AI_MEMORY_FILE'] ?? '');
$legacy = $legacy !== '' ? $legacy : AI_DATA_DIR . '/ai_memory.md';
$legacy = str_replace(['$DATA_DIR', '${DATA_DIR}'], DATA_DIR, $legacy);
$newp = $p !== '' ? str_replace(['$DATA_DIR', '${DATA_DIR}'], DATA_DIR, $p)
: AI_DATA_DIR . '/mem_assisted.md';
if (!file_exists($newp) && file_exists($legacy)) return $legacy;
return $newp;
}
$p = str_replace(['$DATA_DIR', '${DATA_DIR}'], DATA_DIR, $p);
return $p !== '' ? $p : AI_DATA_DIR . '/ai_memory.md';
return $p !== '' ? $p : AI_DATA_DIR . '/mem_learned.md';
}
// A hard ceiling on the learned file, well under the total. Without it a store that grows on its
// own eventually occupies the whole budget and the operator's own memory is what gets truncated
// away — the exact inversion of which one matters.
function vv_ai_memory_learned_max(): int {
$vars = vv_conf_vars();
$n = (int)($vars['AI_MEMORY_LEARNED_MAX_CHARS'] ?? 1200);
return max(0, min($n, vv_ai_memory_max()));
}
// Which profiles a slot is given to. "*" means every profile. Narrowing it is how a General Chat
// question about bash syntax stops carrying this machine's PCIe topology.
function vv_ai_memory_applies(string $kind, string $profile): bool
{
$vars = vv_conf_vars();
$key = $kind === 'learned' ? 'AI_MEMORY_LEARNED_PROFILES' : 'AI_MEMORY_ASSISTED_PROFILES';
$spec = trim($vars[$key] ?? '*');
if ($spec === '' || $spec === '*') return true;
if ($profile === '') return true;
foreach (explode(',', $spec) as $one) {
if (strcasecmp(trim($one), $profile) === 0) return true;
}
return false;
}
function vv_ai_memory_max(): int {
@@ -620,9 +671,57 @@ function vv_ai_memory_max(): int {
// Truncates rather than refusing: an over-long memory file should cost its own tail, not the
// whole conversation, and the notice tells the model its knowledge is incomplete rather than
// letting it assume it saw everything.
function vv_ai_memory_read(): array {
$p = vv_ai_memory_path();
// Assembles what this profile actually gets, as two separately-labelled blocks so the caller can
// state a different precedence for each. Budget is applied to the pair: learned is capped by its
// own ceiling first, then trimmed against whatever the assisted file leaves — assisted is never
// shortened to make room for learned.
function vv_ai_memory_assemble(string $profile = ''): array
{
$max = vv_ai_memory_max();
$out = ['assisted' => '', 'learned' => '', 'chars' => 0,
'truncated' => false, 'files' => []];
$read = function (string $kind) use ($profile, &$out): string {
if (!vv_ai_memory_applies($kind, $profile)) return '';
$p = vv_ai_memory_path($kind);
if (!file_exists($p)) return '';
$t = trim((string)@file_get_contents($p));
if ($t === '') return '';
$out['files'][] = ['kind' => $kind, 'path' => $p, 'chars' => mb_strlen($t)];
return $t;
};
$assisted = $read('assisted');
$learned = $read('learned');
// The operator's file gets the budget first, in full. If it alone exceeds the cap that is a
// problem to report, not one to solve by dropping the other slot silently.
if (mb_strlen($assisted) > $max) {
$assisted = mb_substr($assisted, 0, $max) . "\n\n[assisted memory truncated at {$max} characters]";
$out['truncated'] = true;
$learned = '';
} else {
$room = min(vv_ai_memory_learned_max(), $max - mb_strlen($assisted));
if ($room <= 0) {
$learned = '';
} elseif (mb_strlen($learned) > $room) {
// Cut on a line boundary — half a sentence asserted as fact is worse than one fewer.
$learned = mb_substr($learned, 0, $room);
$cut = mb_strrpos($learned, "\n");
if ($cut !== false && $cut > 0) $learned = mb_substr($learned, 0, $cut);
$out['truncated'] = true;
}
}
$out['assisted'] = $assisted;
$out['learned'] = $learned;
$out['chars'] = mb_strlen($assisted) + mb_strlen($learned);
return $out;
}
function vv_ai_memory_read(string $kind = 'assisted'): array {
$p = vv_ai_memory_path($kind);
$max = $kind === 'learned' ? vv_ai_memory_learned_max() : vv_ai_memory_max();
if (!file_exists($p)) return ['text' => '', 'chars' => 0, 'truncated' => false, 'exists' => false];
@@ -641,13 +740,14 @@ function vv_ai_memory_read(): array {
// Atomic, and refuses to exceed the cap. The cap is a context budget shared with retrieval and
// reasoning on every turn, so it is enforced on write rather than silently trimmed on read.
function vv_ai_memory_write(string $text): array {
$p = vv_ai_memory_path();
$max = vv_ai_memory_max();
function vv_ai_memory_write(string $text, string $kind = 'assisted'): array {
$p = vv_ai_memory_path($kind);
$max = $kind === 'learned' ? vv_ai_memory_learned_max() : vv_ai_memory_max();
$len = mb_strlen($text);
if ($len > $max) {
return ['ok' => false, 'error' => "Memory is {$len} characters; the limit is {$max}. "
$what = $kind === 'learned' ? 'Learned memory' : 'Memory';
return ['ok' => false, 'error' => "{$what} is {$len} characters; the limit is {$max}. "
. "It is included in every prompt, so it competes with retrieval for context."];
}