Files
agentik-desktop/STORAGE.md
T

251 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Где что хранится на диске
**Вопрос:** где живут настройки, кэш истории и список диалогов.
**Ответ коротко:** всё лежит в одном каталоге клиента. Никакой базы не нужно —
история и есть поток строк, а список диалогов вообще кэшировать не надо так,
как историю. Ниже — почему, и что для этого уже есть в библиотеке.
Всё сверено с исходниками `agentik`, не по памяти.
---
## 1. Карта: что где лежит
```
~/.agentik/ каталог клиента (на Windows — соответствующий системный)
settings.json настройки: агенты, адреса, тема, выбранная группа
groups.json группы и в какой группе какой диалог
dialogs.json снимок списка диалогов — чтобы окно не было пустым
read.json докуда дочитан каждый диалог (для счётчиков)
history/
<id диалога>.jsonl сообщения: одна строка — одно сообщение
blobs/
<id сообщения> картинки из сообщений, отдельными файлами
```
Один каталог — простое правило: **удалил каталог, клиент чистый.** Ничего не
прячется в других местах.
### Почему файлы, а не база
История диалога — **только дописывается**: сообщений не правят и не удаляют
(в библиотеке так и написано: «Только `insert` и чтение. Никаких обновлений»).
Для дописываемого потока база не нужна:
- **Строка на сообщение.** Читаем конец файла — знаем последнее сообщение.
Дописываем в конец — вот и весь кэш.
- **Понятно человеку.** Открыл файл — увидел сообщения. В случае беды можно
посмотреть глазами и починить руками.
- **Нет лишней зависимости.** База — это драйвер, версии, миграции схемы.
Плюс это прямо соответствует тому, ради чего мы вообще выбрали десктоп
эталоном: код, который **можно прочитать и понять**.
---
## 2. Настройки — один файл
Пара агентов, адреса, тема, выбранная группа. Это десяток полей —
им не нужна база и не нужен отдельный файл на каждую настройку.
Важно: **настройки человек правит руками.** Если клиент однажды не запустится из-за
кривого адреса, должно быть можно открыть `settings.json`, увидеть и исправить.
Поэтому формат — обычный читаемый, а не сжатый.
Отдельно: **секреты не в этом файле.** Если у агента будет пароль/ключ,
он кладётся в системное хранилище паролей, а в настройках остаётся только
ссылка на него. Иначе ключ утечёт вместе с настройками, которые человек
может кому-то переслать.
---
## 3. Группы — они существуют только у нас
**Это важное. Сервер про группы ничего не знает.**
В библиотеке диалог описывается шестью полями: `id`, `title`, `isTemporal`,
`createdAt`, `updatedAt` и два признака про картинки. **Поля «группа» там нет.**
Значит, группы — целиком наша выдумка на стороне клиента, и жить они будут
в `groups.json`.
**Что из этого следует, и это надо решить:** телефон и десктоп **разойдутся**.
Создал группу «Работа» на десктопе — на телефоне её нет. Отсюда два пути:
1. **Группы только на устройстве.** Просто, но раскладка не ездит между
устройствами. Для одного человека за одним компьютером — нормально.
2. **Группы на сервере.** Тогда нужно добавить в библиотеку хранение групп —
это уже не «просто клиент», это расширение самого agentik.
Пока в макетах нарисован путь 1. Если хотим 2 — это отдельное решение,
и его лучше принять **до** того, как начнём писать клиент.
---
## 4. Кэш истории — файл на диалог
Схема целиком описана в `CACHE.md`. Здесь — только где лежит.
**Файл на диалог, одна строка на сообщение.** Причина простая: диалоги друг
с другом не связаны. Открыли один — читаем один файл, чужого не касаемся.
Заодно один битый файл не портит остальные.
**Как читаем:** храним в памяти, на чём остановились (последняя прочитанная
строка). При открытии диалога читаем конец файла — это последнее сообщение,
с него и спрашиваем сервер «что новее». Весь файл при каждом открытии не
перечитываем.
**Картинки — отдельно.** В сообщении картинка едет как массив байт. Держать
её внутри строки истории нельзя: файл распухнет, и станет нечитаемым.
Поэтому картинка кладётся в `blobs/<id сообщения>`, а в истории остаётся
ссылка на неё.
### Три риска, честно
1. **Обрыв записи.** Клиент упал посреди записи строки — в конце файла
остался обрубок. Тогда: обрубок отбрасываем, спрашиваем сервер заново
по последней целой строке. Так как сервер — источник правды, потеря
не страшна.
2. **Очень длинный диалог.** Файл растёт. Пока спасает то, что читаем конец,
а не весь файл. Если однажды станет тяжело — режем историю на части
по месяцам, но это потом, не сейчас.
3. **Файлов много.** Да, на каждый диалог свой файл. Но файлы мелкие,
а система умеет держать миллионы файлов. Проблемой это станет на порядки
позже, чем что-то другое.
---
## 5. Список диалогов — кэшировать не надо (в главном)
Вот тут отличие от истории, и оно важное.
| | История диалога | Список диалогов |
|---|---|---|
| Сколько данных | много сообщений, каждое с текстом | сто строк, в каждой имя и дата |
| Размер одной порции | сотни килобайт | несколько килобайт |
| Растёт | на каждом ответе агента | медленно |
**Вывод: список диалогов надо честно спрашивать у сервера.** Причины:
- **Он лёгкий.** Сто диалогов — это несколько килобайт. Одна быстрая просьба,
а не выкачивание истории.
- **Он всегда свежий.** Список меняется от чужой работы агента (он может вести
другой диалог, пока мы смотрим этот). Кэшировать его и синхронизировать
приростом — это сложная механика ради нескольких килобайт.
- **Много диалогов — уже решено.** В библиотеке есть постраничная выдача:
берёт по 100 штук и подгружает следующие страницы по мере надобности.
Отдельно ничего придумывать не надо.
### Но пустое окно при запуске — реальная беда
Проблема не в размере списка, а в том, что **пока он едет, окно пустое.**
Поэтому в карте выше и лежит `dialogs.json` — **снимок** списка с прошлого раза.
Схема такая:
1. Запустились — рисуем список из снимка. Мгновенно, окно не пустое.
2. Одновременно спрашиваем сервер свежий список.
3. Пришёл — заменяем нарисованное целиком.
Это **не кэш**, а занавеска, чтобы не смотреть в пустоту. Никакой сверки
прироста, никаких сложных правил: пришёл свежий список — взяли его целиком.
Снимок всегда считается устаревшим.
Если снимка нет (первый запуск) — показываем «загружаю».
---
## 6. Что в API не хватает
Проверено по коду библиотеки. Вот что есть и чего нет для того, что
нарисовано в макетах.
| Что нужно в списке | Есть в библиотеке? |
|---|---|
| Название диалога | **Есть** — `title` |
| Время последнего изменения | **Есть** — `updatedAt`, список уже отсортирован по свежести |
| Понять, что диалог удалён или переименован | **Есть** — события `Deleted`, `Renamed` |
| Постраничная выдача | **Есть** — по 100 штук |
| **Последнее сообщение строкой** | **Нет!** |
| **Сколько непрочитанных** | **Нет!** |
| **Событие «в диалоге что-то произошло»** | **Нет!** |
| **Группа диалога** | **Нет!** |
Три дырки разберём отдельно — они разные по последствиям.
### 6.1. Превью последнего сообщения — нет
В макете в каждой строке под именем стоит текст: «Собрал отчёт, жду правок».
**Взять его неоткуда.** В списке диалогов только имя и дата.
Варианты:
- **Добавить в библиотеку.** В том же хранилище лежат сообщения — превью
берётся одним запросом рядом со списком. Это правильный путь: один запрос
за списком, как и было.
- **Спрашивать по диалогу.** Сто диалогов — сто запросов. Не годится.
- **Убрать из макета.** Строки станут суше, зато ничего не меняем.
### 6.2. Счётчик непрочитанных — нет
В макете справа в строке стоит «2». **Числа взять неоткуда.**
Тут два уровня, и они разной цены:
- **Точка «есть новое»** — сервер менять **не нужно**. У нас уже есть
`updatedAt` (когда диалог последний раз менялся) и свой `read.json`
(докуда дочитали). Есть новое = `updatedAt` новее нашей отметки.
- **Число непрочитанных** — **нужен сервер.** Чтобы получить «2», надо знать,
сколько сообщений в диалоге всего: тогда число = всего минус прочитанное.
Одно поле в ответе списка, считается по той же базе.
### 6.3. «В диалоге что-то произошло» — события нет
В потоке агента есть три события: **создан, удалён, переименован.**
События «пришло новое сообщение» **нет** — хотя сервер в этот момент как раз
обновляет `updatedAt` диалога (это видно по коду: `touch(id, now)`).
**Что это значит на практике:** агент работает в другом диалоге, а мы в этот
момент смотрим список — **список сам не обновится.** Ни порядок не поедет,
ни точка «есть новое» не загорится, пока мы не спросим сервер заново.
Пути:
- **Обновлять список самому, раз в несколько секунд.** Без изменения API.
Список лёгкий (см. §5) — это честно и дёшево. Плюс обновлять при
возвращении окна в фокус.
- **Добавить событие.** Тогда список живёт сам, без опроса. Но опрос всё
равно остаётся как страховка от обрыва связи.
Первый путь достаточен. Второй — приятная добавка на потом.
### 6.4. Итог: что менять в API
**Минимум, который закрывает макеты:**
1. **Превью последнего сообщения** в ответе списка диалогов.
2. **Число непрочитанных** в ответе списка диалогов.
Обе правки — в том же хранилище, рядом с тем, что уже читается для списка.
Ни новых таблиц, ни новых запросов со стороны клиента: как был один запрос
за списком, так и остался.
**Не обязательно:** событие «в диалоге что-то произошло» — вместо него опрос
раз в несколько секунд. Можно отложить.
**Нужно решить отдельно:** группы (§3). Это не правка ответа, это новое
понятие в библиотеке.
---
## 7. Открытые вопросы
1. **Группы — на устройстве или на сервере?** От этого зависит, поедут ли они
между десктопом и телефоном (§3).
2. **Меняем API: превью и число непрочитанных?** Или убираем их из макета
и живём на том, что есть (§6.4).
3. **Что засчитывать сообщением** при подсчёте непрочитанных: только ответы
агента или ещё вызовы инструментов, которые в истории тоже лежат
отдельными записями.
4. **Бейдж — число или точка?** Точка не требует менять сервер вообще (§6.2).