# memo — заметки как миллион файлов, каждый со своей семантикой Спека v0.1 (только проект, кода нет). Автор идеи — пользователь; форма зафиксирована 02.10.2026. ## 0. Идея в одном абзаце Обычный провайдер памяти в Hermes — **один** стор на весь профиль (MEMORY.md, holographic, mem0). Здесь форма другая: **markdown-файлы лежат в дереве папок и являются истиной**, а рядом с каждой папкой-темой лежит пересобираемый векторный индекс. Коллекций — сколько угодно, они не мешают друг другу, и «спрашивают про X → ищем в X» решается детерминированной маршрутизацией, а не надеждой на автопоиск по всему корпусу. Это не память (маленькая и всегда в промпте) и не файловая система (слепая к смыслу) — это **библиотека со смысловым индексом**. ## 1. Границы: что это и чем не является - НЕ замена MEMORY.md / USER.md. Курируемая память по-прежнему маленькая и всегда в промпте. - НЕ граф знаний, НЕ авто-суммаризация, НЕ «агент сам решает, что запомнить». - НЕ облако: модель эмбеддингов локальная (CPU/ONNX), сеть не нужна ни при индексации, ни при поиске. - НЕ альтернатива grep: файлы остаются человекочитаемыми, любой поиск по тексту работает и без индекса. ## 2. Правило номер один **Markdown — истина, `.db` — кэш, который не жалко.** Следствие: индекс можно удалить целиком и он пересоберётся. Ничего, что существует только в базе, быть не должно. Любая миграция схемы = `rm -rf` + ленивая пересборка при первом обращении к коллекции. ## 3. Модель данных ``` / # корень библиотеки, напр. ~/notes/memo _registry.yaml # реестр коллекций: имя → описание, путь, эмбеддер, счётчики work/ jira/ _meta.yaml # описание коллекции, теги, модель/димы, версия чанкера notes.md # основная заметка (markdown, истина) pitfalls.md .memo/ index.db # sqlite: FTS5 + vec0, пересобираемо state.json # версия чанкера/модели, sweep-время life/ books/ _meta.yaml iphuck10.md .memo/index.db inbox.md # неразобранное; коллекция НЕ индексируется ``` Правила: 1. **Коллекция = папка** (не файл). Один sqlite на заметку — запрещено: inode, соединения, фрагментация. 2. **Заметка = `.md`-файл** внутри коллекции. Ориентир: 10–300 файлов на коллекцию, до ~10⁵ коллекций в теории. 3. **Максимум ~2 уровня вложенности** папок. Глубже — значит тема слишком крупная, её надо дробить. 4. Черновики — в `inbox.md` в корне: не индексируются, агент за них не отвечает. 5. `_meta.yaml` коллекции обязателен: без описания коллекция невидима для маршрутизатора. ```yaml # _meta.yaml name: work/jira description: "Корпоративная Jira: тикеты, флоу, доступы, грабли MCP" tags: [corp, jira, otp] embedder: intfloat/multilingual-e5-small # 384 dims chunker: headings-v1 # меняешь — индекс пересобирается ``` ## 4. Формат заметки - Границы чанков = **markdown-заголовки** (`##`/`###`); слишком длинный раздел режется по ~800 токенов с перекрытием 15%. Никогда не резать по символам «вслепую». - Опциональный frontmatter: `id`, `tags`, `updated`. Обязателен только `updated` (руками или скриптом). - Имена файлов — ASCII-slug, без пробелов: `postgres-restore.md`, не `про восстановление.md`. - В текст заметки полезно писать **явные синонимы и аббревиатуры** («PBR (policy based routing)») — это бесплатно лечит промахи чистого вектора на жаргоне. ## 5. Индекс (одна база на коллекцию) ```sql CREATE TABLE chunks( id INTEGER PRIMARY KEY, path TEXT, heading TEXT, line INTEGER, ord INTEGER, text TEXT, h1 TEXT, mtime REAL, hash TEXT); CREATE VIRTUAL TABLE chunks_fts USING fts5(text, heading, tokenize='unicode61'); CREATE VIRTUAL TABLE chunks_vec USING vec0(embedding float[768]); -- sqlite-vec, dim по модели ``` - **Гибридный поиск:** BM25 (FTS5) + вектор, слияние RRF (`k=60`). Чистый вектор обязателен… и недостаточен: числа, аббревиатуры, имена файлов находятся только лексикой. - **Инкремент:** сравниваем `(path, mtime, size)` со таблицей `files`; переэмбеддиваем только изменившиеся файлы. Полная пересборка коллекции — по требованию. - Версия чанкера/модели в `.memo/state.json`: не совпала — коллекция переиндексируется целиком. - Сортировка результата: RRF-скор, затем свежесть (`mtime`) как тайбрейкер, чтобы свежая заметка выигрывала у устаревшей при равном совпадении. ## 6. Эмбеддер - База: `intfloat/multilingual-e5-small` (384 dims, ~118M параметров, ONNX/CPU через fastembed). Мультиязычность не опция: `nomic-embed-text` на русском молча проигрывает. - Префиксы e5 обязательны: документ — `"passage: …"`, запрос — `"query: …"`; нормализация L2. - Апгрейд до `bge-m3` (1024) — если качество на реальных вопросах не устроит; тогда переиндекс всех коллекций. - **Модель грузится один раз** на процесс, лениво, при первом обращении. Это аргумент за долгоживущий процесс (MCP-сервер), а не за CLI-утилиту. ## 7. Маршрутизация (самое узкое место) Проблема не в поиске, а в выборе **где** искать. Миллион коллекций в промпт не влезет. - До ~50 коллекций: плоский список `name — description` целиком в инструмент (или промпт). - До ~5 000: дерево имён (`work/*`, `work/jira/*`) — фильтр по префиксу + поиск по описаниям. - Больше: отдельный роутер (BM25/эмбеддинг по `description` коллекций) → кандидаты → агент выбирает. - Всегда доступен режим `scope="*"`: искать по всем индексам, но это дорого и только осознанно. ## 8. Интерфейс: инструмент-тень, а не способ записи (v0.2) Решение v0.2 (идея пользователя, 02.10.2026): **агент пишет и читает только markdown своими обычными средствами** (`write_file` / `patch` / `read_file`). Индекс никто через API не наполняет — он наблюдает за файловой системой и догоняет. Инструмент поиска — только чтение, для агента он такой же обычный, как grep. Источник правды один, и это файл. | Инструмент | Аргументы | Возврат | |---|---|---| | `memo_search` | `path` (файл ИЛИ папка), `query`, `k=8`, `mode=hybrid\|lex\|vec` | `path:line`, `heading`, `score`, текст чанка (~1 200 символов) | | `memo_status` | `path` (опц.) | что проиндексировано, когда, отстаёт ли индекс от mtime | | `memo_reindex` | `path`, `full=false` | аварийная пересборка, если watcher ослеп | - `path` принимает **папку** — поиск рекурсивно по коллекции. Типовой вопрос звучит «где в этой папке про X», а не «в этой заметке». - `memo_write` из v0.1 удалён: две дороги записи = два источника правды. - Возврат всегда с `path:line` — агент дочитывает нужное `read_file(offset)`, а не открывает файл целиком. ## 8-bis. Наблюдатель (watcher) - Отдельный процесс: inotify (`watchdog`) → debounce 500 мс → `stat` + `hash` → переэмбеддинг только изменившегося. - **Событие — триггер, не истина.** Решение «изменилось ли» принимает хэш: редакторы бросают пачку событий на одну запись (vim `4913`, atomic rename в VS Code и Obsidian). - **Реконсиляция обязательна:** inotify слепнет на FUSE/sshfs/SMB и теряет события при переполнении очереди. Раз в 10–15 мин — скан `(path, mtime, size)` по коллекции; холодный поиск тоже форсит перескан. - **Два процесса над одним sqlite → WAL** (`journal_mode=WAL`), иначе «database is locked» на живой записи. - Разделение: watcher пишет, MCP-сервер читает. stdio-MCP живёт только внутри сессии Hermes, поэтому фоновая индексация в нём невозможна — watcher это **отдельный сервис** (`/opt/memo/`, systemd или podman-compose), MCP подключается к его базе. Альтернатива «всё в одном» — streamable-http демон со встроенным потоком-watcher. - **Задержки быть не должно:** `memo_search` перед поиском делает быстрый `stat`+`hash` по целевому пути и догоняет разницу сам. Тогда «агент записал и сразу ищет» находит правку сразу, а watcher остаётся страховкой для правок, сделанных человеком и другими инструментами. - Остаётся от v0.1: **карта тем** (раздел 9). Прозрачность файлового слоя не отменяет вопрос «в какой папке искать» — её даёт скилл, а не инструмент. ## 9. Правила записи (скилл, а не код) Файлы гниют не от поиска, а от записи «куда попало». Нужен тонкий слой дисциплины — скилл вида: ``` Тема → коллекция инфра/серверы → infra работа/Jira → work/jira книги → life/books личное → life/misc нет темы → inbox.md (не индексируется) ``` - Новая тема = новая папка + `_meta.yaml` + строчка в скилле. Один раз. - Дедуп: перед записью — `memo_search` по этой же коллекции; хэш точной копии не пишем дважды. - Заметка в двух местах — запрещено; при переносе файла индекс коллекции переиндексируется, история mtime сохраняется. ## 10. Что проверяем до того, как что-то оптимизировать 1. 20–30 **реальных** вопросов к своим заметкам (не «тестовых») — вручную смотрим recall@5. 2. Отдельно — вопросы на точные значения: числа, версии, имена хостов, аббревиатуры (тут решает BM25). 3. Отдельно — вопросы «смысловые», другими словами, чем в заметке (тут решает вектор). 4. Замер: холодная индексация коллекции на N файлов, горячий поиск. Целевые ориентиры: поиск < 100 мс. Если recall@5 на реальных вопросах ниже ~0.8 — виноват не индекс, а формулировки заметок и slug'и. ## 11. Открытые развилки (решать после прототипа, не сейчас) - Корень библиотеки: `~/notes/memo` или внутри конкретного репо? - Один MCP-сервер на библиотеку или свой процесс на коллекцию? (Сейчас: один на всё, коллекции — аргумент.) - Нужен ли `memo_gc` (перемещение/удаление) или файлы двигает только человек? - Автоматический захват сессий в `inbox.md` (крон) — или только ручные записи агента. ## 12. Чем отличается от готового в Hermes | | провайдер Hermes (holographic / mem0) | memo | |---|---|---| | Стор | один на профиль | много папок, по теме | | Истина | база данных | markdown на диске | | Пересборка | миграция | удалить `.db` | | Гранулярность | факт/эпизод | заметка/раздел | | Маршрутизация | не нужна | требуется (реестр + scope) | Вывод: для Hermes-агента это не замена провайдера, а дополнение «библиотека»; для своего агента — самостоятельный сервис, подключаемый по MCP. ## 13. Портатив на Kotlin — из чего собирается (проверено 02.10.2026) Разведка нашла всё, о чём говорил пользователь. Своего кода нужно мало. | Нужда | Что берём | Где лежит | Координаты / состояние | |---|---|---|---| | SQLite + **вектор** + FTS5 | `caffeine-mgn/ksqlite` (наш, GitHub) | GitHub `caffeine-mgn/ksqlite`, README.ru есть | Maven Central: `pw.binom.db:ksqlite:0.1.4`. Внутри амальгама SQLite 3.53.4 + **sqlite-vec 0.1.9** (авто-регистрация), FTS3/4/5, RTREE, JSON1. JVM (JNI `.so`) + linux/mingw/Android/a… klib | | Текстовый эмбеддинг | `subochev/text-embedding-kmp` — SigLIP2 int8, **768 dims** | git.binom.pw | caffeine: `pw.binom.ai.embeddingtext:api` / `:siglip` — актуальная **5**. Модель с `http://static.binom.pw/models/siglip2/` | | Текст + vision в одном | `subochev/embedder-kmp` | git.binom.pw | caffeine: `pw.binom.embedder:embedder-api-text` / `-impl-text` (и vision) — **1**. Модели в артефакт не кладутся, пути отдаёт потребитель | | Qdrant (если б понадобился) | `subochev/qdrant` (core/ktor/strong) | git.binom.pw | в caffeine **не выложен**, и это HTTP-клиент к внешнему серверу → при embedded-схеме не нужен | | MCP (stdio + http) | `subochev/mcp` (server-shared / server-cli / server-http, `AITool`) | git.binom.pw | **не опубликован** (наружу только `shared`) → либо submodule/исходники, либо свой MCP на Ktor | | Слежение за файлами | **готового нет** | — | JVM: `java.nio.file.WatchService` (inotify под капотом), register RECURSIVE + debounce + reconcile — пишем сами, ~150 строк | Вывод разведки: «клёвая библиотека для векторного поиска» = **sqlite-vec внутри ksqlite**, а не отдельный Qdrant. Это ровно та схема, что в разделе 5 спеки — то есть зависимость одна: `pw.binom.db:ksqlite` + наш эмбеддер. ### Модули (JVM-first, один Gradle-проект `memo`) 1. `memo-core` — чанкер по заголовкам, хэши/`files`-таблица, схема БД через ksqlite, инкрементальная переиндексация, гибридный поиск (FTS5 BM25 + `vec0` KNN + RRF). 2. `memo-watch` — демон: WatchService на корень, debounce 500 мс, очередь, reconcile раз в 10–15 мин, WAL. 3. `memo-mcp` — MCP stdio+http: `memo_search` / `memo_status` / `memo_reindex`. 4. `memo-cli` — `memo index|search|status` для ручных проверок и приёмки без агента. `core` не знает ни про MCP, ни про watcher — это драйверы. Так ядро потом переиспользуется на Android (`-android`/`-jvm` артефакты уже есть у обеих библиотек). ### Что придётся написать руками Чанкер, RRF-слияние, watcher с реконсиляцией, схему/миграцию, MCP-инструменты. Ориентир суммарно: **600–900 строк Kotlin**. Всё остальное — готовые артефакты из таблицы. ### Приёмка (то, что пойдёт в ТЗ для opencode) 1. `memo index ~/notes/memo` создаёт `~/notes/memo/<тема>/.memo/index.db`, повторный прогон — no-op. 2. `memo search "<смысловой вопрос>"` находит нужный раздел (recall@5 на 20 реальных вопросах). 3. Вопрос с числом/аббревиатурой находит точное совпадение (проверка BM25, не вектора). 4. Файл, поправленный руками (`echo >> note.md`), виден в поиске ≤1 с; правка, сделанная сразу перед поиском, видна **в том же вызове** (stat-догон). 5. MCP-сервер проходит протокольный зонд: `tools/list` + `tools/call` с непустым результатом. 6. Без сети: эмбеддинг и поиск работают при выключенном внешнем интерфейсе. ### Развилка, зафиксированная по умолчанию Берём **JVM** (демон + watcher + CLI + MCP — серверная история, KMP-таргеты тут ничего не дают). Android-таргеты — вторым шагом и без watcher (там индексация по запросу): библиотеки это позволяют.