Split memory into assisted and learned, and deny learned the precedence assisted has
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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."];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user