Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Память

Что туда попадает

Решение о записи принимает сама Ева, и правило об этом живёт в ядре, а не в персоне: персона — файл оператора, и при своей 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 всё равно наступает. Фазы этапа памяти:

  1. Гигиена без модели — доэмбед упавших векторов, снятие архива старше archive_ttl.
  2. Кластеризация без модели — граф похожести по лексике (Jaccard основ ≥ cluster_jaccard) и семантике (косинус ≥ cluster_cosine); связные компоненты — кластеры, крупнее max_cluster — режутся по слабейшим рёбрам. core участвует наравне: противоречие «core против свежего факта» ловится только так.
  3. Разбор кластеров — один вызов модели на кластер: слить дубли, развести противоречие в пользу свежего (или переписать с датировкой, когда старое состояние ещё имеет смысл), расщепить запись-склейку, вывести из череды однотипных событий факт. Устоявшиеся кластеры (consolidated_at новее правок) модели не уезжают — вторая ночь подряд на неизменном корпусе не стоит ни одного вызова.
  4. Забывание вне кластеров — кандидатов считает ядро арифметикой (старое по полураспаду, ни разу не вспомненное, важность ≤ 2, не core); модель может список только сократить — добавить в него нельзя.
  5. Виды, важность, бюджет core — один вызов на весь корпус в сжатом виде: поднять в core то, к чему обращаются постоянно, опустить оттуда мёртвое, перекалибровать сползшую важность, ужать core, переросший свою долю бюджета секции.

Решает модель этапа (sleep.memory.modelllm.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.