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 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
|
# 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.
|
# share off the top of every single turn, so keep it short and factual.
|
||||||
AI_MEMORY_FILE="${AI_DATA_DIR}/ai_memory.md"
|
# Two slots, and which one a fact lands in decides how much authority it carries.
|
||||||
AI_MEMORY_MAX_CHARS=4000 # ~1000 tokens — truncated with a notice if exceeded
|
#
|
||||||
|
# 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 ━━━
|
# ━━━ AI Stored Conversations ━━━
|
||||||
# How many past conversations the AI tab and the Monitor tab's AI row keep. One JSON file per
|
# 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";
|
. "somewhere else, answer anyway and tell them plainly where it actually is.\n\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
$mem = vv_ai_memory_read();
|
// Two blocks, two different standings, and the difference is stated to the model rather than
|
||||||
if ($mem['exists'] && trim($mem['text']) !== '') {
|
// 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"
|
$system .= "WHAT YOU ALREADY KNOW ABOUT THIS OPERATOR AND INSTALLATION\n"
|
||||||
. "Written by the operator, not retrieved. Treat it as established fact about this "
|
. "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 "
|
. "system, prefer it over anything in the passages that contradicts it, and do not "
|
||||||
. "cite it as a numbered source.\n\n"
|
. "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 !== '') {
|
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
|
// 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.
|
// repository and therefore out of the index by construction.
|
||||||
|
|
||||||
function vv_ai_memory_path(): string {
|
// Two memory slots, and the distinction between them is the whole point.
|
||||||
$cfg = vv_ai_config();
|
//
|
||||||
|
// 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();
|
$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);
|
$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 {
|
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
|
// 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
|
// whole conversation, and the notice tells the model its knowledge is incomplete rather than
|
||||||
// letting it assume it saw everything.
|
// letting it assume it saw everything.
|
||||||
function vv_ai_memory_read(): array {
|
// Assembles what this profile actually gets, as two separately-labelled blocks so the caller can
|
||||||
$p = vv_ai_memory_path();
|
// 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();
|
$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];
|
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
|
// 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.
|
// reasoning on every turn, so it is enforced on write rather than silently trimmed on read.
|
||||||
function vv_ai_memory_write(string $text): array {
|
function vv_ai_memory_write(string $text, string $kind = 'assisted'): array {
|
||||||
$p = vv_ai_memory_path();
|
$p = vv_ai_memory_path($kind);
|
||||||
$max = vv_ai_memory_max();
|
$max = $kind === 'learned' ? vv_ai_memory_learned_max() : vv_ai_memory_max();
|
||||||
$len = mb_strlen($text);
|
$len = mb_strlen($text);
|
||||||
|
|
||||||
if ($len > $max) {
|
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."];
|
. "It is included in every prompt, so it competes with retrieval for context."];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user