Give the assistant a standing memory file

A small operator-written file handed to the model at the start of every
conversation — who you are, how this install is set up, what has already been
decided. Injected ahead of the retrieved passages and marked operator-authored
so it outranks anything they contradict, and never cited as a source.

Deliberately not indexed and deliberately under DATA_DIR: it changes
constantly, vector similarity is the wrong way to retrieve things you were
told to remember, and gitignoring it keeps personal notes out of a pushed
repository. The character cap is a context budget — this text costs its share
of 16k on every single turn.
This commit is contained in:
Gmer4Lfe
2026-08-03 17:11:27 -04:00
parent 7625e923e5
commit d0b3588f6c
5 changed files with 181 additions and 0 deletions
+73
View File
@@ -67,6 +67,7 @@
// Status vv_ai_stats(), vv_ai_index_stats(), vv_ai_runtime_stats(), vv_ai_index_meta()
// Models vv_ai_models_available(), vv_ai_models_loaded()
// Diagnosis vv_ai_health(), vv_ai_recent_logs()
// Memory vv_ai_memory_path(), vv_ai_memory_read(), vv_ai_memory_write()
// Retrieval vv_ai_retrieve()
// Jobs vv_ai_job_dir(), vv_ai_job_path(), vv_ai_job_read()
//
@@ -452,6 +453,78 @@ function vv_ai_retrieve(string $query, string $kind = '', string $section = '',
'scanned' => $d['scanned'] ?? 0];
}
// ── Memory ───────────────────────────────────────────────────────────────────────────────────
//
// A small operator-maintained file handed to the model at the start of every conversation.
// Injected, never indexed: it changes constantly, and vector similarity is the wrong retrieval
// 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();
$vars = vv_conf_vars();
$p = trim($vars['AI_MEMORY_FILE'] ?? '');
$p = str_replace(['$DATA_DIR', '${DATA_DIR}'], DATA_DIR, $p);
return $p !== '' ? $p : DATA_DIR . '/ai_memory.md';
}
function vv_ai_memory_max(): int {
$vars = vv_conf_vars();
$n = (int)($vars['AI_MEMORY_MAX_CHARS'] ?? 4000);
return max(200, min($n, 20000));
}
// Returns ['text'=>string,'chars'=>int,'truncated'=>bool,'exists'=>bool].
// 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();
$max = vv_ai_memory_max();
if (!file_exists($p)) return ['text' => '', 'chars' => 0, 'truncated' => false, 'exists' => false];
$raw = (string)@file_get_contents($p);
$len = mb_strlen($raw);
if ($len <= $max) {
return ['text' => $raw, 'chars' => $len, 'truncated' => false, 'exists' => true];
}
return [
'text' => mb_substr($raw, 0, $max) . "\n\n[memory truncated at {$max} characters]",
'chars' => $len,
'truncated' => true,
'exists' => true,
];
}
// 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();
$len = mb_strlen($text);
if ($len > $max) {
return ['ok' => false, 'error' => "Memory is {$len} characters; the limit is {$max}. "
. "It is included in every prompt, so it competes with retrieval for context."];
}
$dir = dirname($p);
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
return ['ok' => false, 'error' => 'Cannot create ' . $dir];
}
$tmp = $p . '.vv.tmp';
if (@file_put_contents($tmp, $text) === false) {
return ['ok' => false, 'error' => 'Cannot write ' . $tmp];
}
if (!@rename($tmp, $p)) {
@unlink($tmp);
return ['ok' => false, 'error' => 'Cannot install ' . $p];
}
return ['ok' => true, 'chars' => $len];
}
function vv_ai_job_dir(): string {
if (!is_dir(VV_AI_JOB_DIR)) @mkdir(VV_AI_JOB_DIR, 0700, true);
return VV_AI_JOB_DIR;