Files
memo/docs/SPEC.md
T

20 KiB
Raw Blame History

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. Модель данных

<root>/                          # корень библиотеки, напр. ~/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 коллекции обязателен: без описания коллекция невидима для маршрутизатора.
# _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. Индекс (одна база на коллекцию)

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<T>) 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 (там индексация по запросу): библиотеки это позволяют.