Память
Что туда попадает
Решение о записи принимает сама Ева, и правило об этом живёт в ядре, а не в
персоне: персона — файл оператора, и при своей persona.file память
осталась бы в промпте только на чтение. Критерий один и общий — обеднеет ли
следующая Ева, в другом чате, не зная этого. Тогда memory_save, по ходу
разговора, без спроса и без церемонии.
Отсечка с трёх сторон: не память то, на что отвечает история перед глазами;
то, что можно перечитать из файла или инструмента; то, что живёт до конца
задачи — это блокнот. Одна тема — одна запись: memory_update
правит сохранённое (включая вид: эпизод, записанный в core по ошибке,
опускается в fact), memory_forget убирает устаревшее. Ответ на
сохранение показывает похожие записи — момент записи единственный, когда
видно, что чему противоречит; иначе поправка ляжет рядом со старой правдой и
обе всплывут потом как равные.
Запись можно пометить личной (private): тело Господина, здоровье, деньги,
местоположение, документы, чьи-то имена. Личное живёт только в турах
Господина из доверенного окружения (trusted) — там, где он работает сам:
TUI, андроид, его собственная личка с Евой. В любом другом месте его нет ни
в промпте, ни в выдаче memory_search, ни в соседях при сохранении, а
memory_list там не показывается вовсе: дамп идёт вместе с личным.
Допуск считается по аудитории, а не по говорящему. Мастерство тура само по себе ничего не открывает: в группе спрашивает Господин, а ответ читают все, кто там сидит. Тур Господина из недоверенного места получает полную выборку без личного и сноску о том, что личное придержано, — иначе модель принимает вырезанное за незнание и досочиняет.
Записи, сделанные до появления пометки, считаются общими: разметить их — решение Господина, а не догадка миграции.
Правило висит в кэшируемой части промпта и только в турах Господина —
мутирующие memory_* заперты на него, чужому туру это был бы вес без права.
Виды и важность в правиле не пересказываются: их объясняет схема самого
инструмента, которую модель читает ровно в момент вызова.
Как достаётся
Оценка воспоминания против запроса:
score = relevance × (0.35 + 0.65 × 0.5^(возраст / half_life)) × (важность / 3) × частотный бонус
где relevance — BM25 (нормированный), а при заданной
memory.embedding_model — гибрид 50/50 с семантической близостью;
важность ∈ 1..=5. Близость считается не сырым косинусом, а
(cos − 0.4) / (1 − 0.4): у эмбеддингов любые два текста похожи довольно
сильно (на моём корпусе с bge-m3 медиана заведомо несвязанных пар ≈ 0.39,
родство начинается от ≈ 0.7), и сырой косинус отдавал бы половину веса
скоринга за то, что оба текста написаны по-русски. Ноль шкалы стоит на
фоне — свой корпус стоит перемерить, если модель эмбеддингов другая. Возраст меряю от
последнего обращения, не только создания — что вспоминаю, то помню дольше
(spaced repetition, как у людей). Явный memory_search подкрепляет
найденное; пассивное вплетение в промпт — нет, иначе всплывшее однажды жило
бы вечно. Вид core минует скоринг и вплетается в промпт всегда — но в
выдачу memory_search не попадает: тул отдаёт только релевантное запросу,
иначе он возвращал бы модели текст, который та уже прочла в секции
# Memory. event выцветает
вчетверо быстрее fact. Сохранение с дедупом: достаточно похожая запись
(Jaccard основ ≥ 0.7) не плодится, а подкрепляется, причём более подробная
формулировка побеждает — повторяют факт обычно ради уточнения.
Строка воспоминания несёт возраст и важность (id | kind | age | importance | content): без возраста вчерашняя правда читается наравне с годовалой.
Выборка ограничена не только счётом (memory.top_k), но и порогом
относительной силы: всё слабее memory.recall_floor (0.15) от ЛУЧШЕГО
счёта в этой же выборке отбрасывается. Счёт без порога всегда добирает
список до конца, и запись, разделившая с запросом одно частое слово,
занимает место наравне с попаданием. Порог именно относительный: BM25
между запросами не нормирован, и абсолютная граница резала бы наугад.
core идёт мимо скоринга и порогом не задет.
Секция памяти в промпте ограничена бюджетом memory.prompt_budget (8000
символов по умолчанию), и бюджет делится: core занимает не больше 60% —
иначе разросшийся core вытесняет выборку целиком, и весь скоринг работает
вхолостую. За краем доли первым выпадает наименее важное, а из равного по
важности — молодое; о выпавшем секция говорит вслух и просит свернуть лишнее,
потому что молча обрезанная память читается как «это всё, что я помню». В
чужих публичных турах выборка режется до memory.public_top_k; личное не
показывается ни там, ни в турах Господина из недоверенного места — этим
выборка не режется.
Что из этого доехало до модели, видно снаружи: GET /v1/memories/prompt?q=…
отдаёт секцию ровно в том виде, в каком её несёт тур (&public=true — как
из-за пределов доверенного окружения), и ничего при этом не подкрепляет.
Эмбеддинги досчитываются батчами на старте — и тем, у кого вектора нет, и тем, у кого он посчитан другой моделью: пространства разных моделей несопоставимы, и косинус между ними — шум с весом 0.5. Косинус считается только с вектором активной модели; всё это best-effort — не смогли, работаем на лексике.
Сон: консолидация
Без ухода память только копится: дедуп ловит лишь лексически похожее,
полураспад прячет старое, но не удаляет, противоречия сосуществуют, а
важность сползает в сплошные 4–5. Поэтому раз в сутки (секция sleep
конфига) ядро спит — как человеческий медленноволновой сон
переплавляет эпизоды в знание, так этот перебирает весь корпус и приводит
его в порядок.
Сон дожидается тишины (разговор отшумел idle_for, ни одной живой
генерации — крон и подагенты считаются) и идёт именованными этапами: за
консолидацию отвечает этап memory (секция sleep.memory), после него
идёт этап tools — курирование наборов инструментов.
Мелкий корпус (min_memories) пропускает только этап памяти: ночь с одним
этапом tools всё равно наступает. Фазы этапа памяти:
- Гигиена без модели — доэмбед упавших векторов, снятие архива старше
archive_ttl. - Кластеризация без модели — граф похожести по лексике (Jaccard основ ≥
cluster_jaccard) и семантике (косинус ≥cluster_cosine); связные компоненты — кластеры, крупнееmax_cluster— режутся по слабейшим рёбрам.coreучаствует наравне: противоречие «core против свежего факта» ловится только так. - Разбор кластеров — один вызов модели на кластер: слить дубли,
развести противоречие в пользу свежего (или переписать с датировкой,
когда старое состояние ещё имеет смысл), расщепить запись-склейку,
вывести из череды однотипных событий факт. Устоявшиеся кластеры
(
consolidated_atновее правок) модели не уезжают — вторая ночь подряд на неизменном корпусе не стоит ни одного вызова. - Забывание вне кластеров — кандидатов считает ядро арифметикой (старое по полураспаду, ни разу не вспомненное, важность ≤ 2, не core); модель может список только сократить — добавить в него нельзя.
- Виды, важность, бюджет core — один вызов на весь корпус в сжатом
виде: поднять в
coreто, к чему обращаются постоянно, опустить оттуда мёртвое, перекалибровать сползшую важность, ужатьcore, переросший свою долю бюджета секции.
Решает модель этапа (sleep.memory.model → llm.model.smartest →
general), с персоной в системном промпте — «что важно» есть функция
характера, и безличный судья выбросил бы ровно то отношенческое, что делает
Еву Евой, — и со всегда включённым рассуждением, поверх глобальной
настройки. Действовать во сне нельзя: единственный инструмент —
sleep_finish, которым модель отдаёт план фазы и тем её закрывает; его
аргументы и есть ответ, за вызовом ничего не идёт. Формы операций и их
смысл живут в схеме этого инструмента, а не в промпте: просить план прозой
с JSON внутри — значит регулярно получать обёртки, заборы и обрубки
(разбор прозы остался страховкой на апстрим, не понявший форс вызова).
Потолка ответа у планового вызова нет: он общий с рассуждением, и высокая
ступень съедала чатовый потолок целиком, обрывая план на полуслове.
Каждая операция обязана нести why, а применяет план ядро — с
предохранителями: max_ops и отдельный max_forget на ночь, core не
удаляется никогда (только понижение вида), записи моложе keep_recent не
забываются и не переписываются, private не снимается и заражает слияние.
Слияние сохраняет id подкреплённейшей записи и суммирует историю обращений —
spaced repetition не обнуляется.
Всё забытое — сном, тулом memory_forget или ручкой — лежит в архиве
archive_ttl и возвращается POST /v1/memories/archive/{id}/restore:
стирание обратимо, потому что память — единственное, чего не восстановишь
из репозитория и логов. Журнал ночей — GET /v1/sleep/runs, о заметной
ночи Господину уходит уведомление.
dry_run (общий на все этапы, включая tools) считает планы и пишет их в
журнал, не применяя, — режим первых недель.
Спящая Ева не отвечает: тур встаёт в очередь до пробуждения, его
SSE-поток открывается событием sleeping — клиент показывает «спит…» и
кнопку «Разбудить» (POST /v1/sleep/wake; недоделанная фаза при этом не
применяется, применённые остаются, срок следующего сна не сгорает). Уснуть,
не дожидаясь ночи, — POST /v1/sleep или тул memory_consolidate.