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
+26 -2
View File
@@ -1735,8 +1735,32 @@
# The character cap is a context budget, not a style guide. At 16384 the retrieved passages,
# the model's reasoning and the conversation history are already competing; memory takes its
# share off the top of every single turn, so keep it short and factual.
AI_MEMORY_FILE="${AI_DATA_DIR}/ai_memory.md"
AI_MEMORY_MAX_CHARS=4000 # ~1000 tokens — truncated with a notice if exceeded
# Two slots, and which one a fact lands in decides how much authority it carries.
#
# assisted written by the operator. The prompt tells the model to prefer it over retrieved
# passages — a human asserting a fact about their own machine outranks a doc that
# may be stale. This is the file the AI tab edits.
# learned proposed by the assistant, accepted by the operator. A hint only: retrieval
# overrules it, and it is trimmed first when the budget bites. Deliberately the
# weaker seat, because memory the model writes AND the prompt ranks above the source
# code would let one wrong conclusion restate itself forever.
#
# AI_MEMORY_FILE is the pre-split name and is still honoured: while the assisted file does not
# exist, the legacy path is read instead, so an upgrade loses nothing.
AI_MEMORY_ASSISTED_FILE="${AI_DATA_DIR}/mem_assisted.md"
AI_MEMORY_LEARNED_FILE="${AI_DATA_DIR}/mem_learned.md"
AI_MEMORY_FILE="${AI_DATA_DIR}/ai_memory.md" # legacy — read only if the assisted file is absent
# Which profiles each slot is given. "*" is all of them. Narrowing is how a General Chat question
# about bash syntax stops carrying this machine's PCIe topology and disk serials.
AI_MEMORY_ASSISTED_PROFILES="*"
AI_MEMORY_LEARNED_PROFILES="*"
AI_MEMORY_MAX_CHARS=4000 # ~1000 tokens — the budget for both slots together
# A ceiling on the learned slot alone, well under the total. A store that grows on its own would
# otherwise end up occupying the whole budget, and the operator's own memory is what would get
# truncated away — the exact inversion of which one matters.
AI_MEMORY_LEARNED_MAX_CHARS=1200
# ━━━ AI Stored Conversations ━━━
# How many past conversations the AI tab and the Monitor tab's AI row keep. One JSON file per
+19 -3
View File
@@ -746,13 +746,29 @@ if ($scope !== '') {
. "somewhere else, answer anyway and tell them plainly where it actually is.\n\n";
}
$mem = vv_ai_memory_read();
if ($mem['exists'] && trim($mem['text']) !== '') {
// Two blocks, two different standings, and the difference is stated to the model rather than
// implied. The assisted block keeps the precedence it always had. The learned block explicitly
// does not get it: those lines were proposed by a model, and a model whose own notes are ranked
// above the retrieved source would restate a wrong conclusion indefinitely, growing more
// confident each time it read its own claim back.
$mem = vv_ai_memory_assemble($profile);
if (trim($mem['assisted']) !== '') {
$system .= "WHAT YOU ALREADY KNOW ABOUT THIS OPERATOR AND INSTALLATION\n"
. "Written by the operator, not retrieved. Treat it as established fact about this "
. "system, prefer it over anything in the passages that contradicts it, and do not "
. "cite it as a numbered source.\n\n"
. trim($mem['text']) . "\n\n";
. trim($mem['assisted']) . "\n\n";
}
if (trim($mem['learned']) !== '') {
$system .= "NOTES YOU WROTE EARLIER, KEPT BY THE OPERATOR\n"
. "These were proposed by you on previous turns and approved for keeping. They are "
. "hints, not facts. Anything in the passages above, and anything in the operator's "
. "own notes, outranks them — if a passage contradicts a note here, the passage is "
. "right and the note is stale. Do not cite them as numbered sources, and do not "
. "repeat one as established fact if nothing retrieved supports it.\n\n"
. trim($mem['learned']) . "\n\n";
}
if ($diagBlock !== '') {
+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."];
}