16 KiB
Где что хранится на диске
Вопрос: где живут настройки, кэш истории и список диалогов.
Ответ коротко: всё лежит в одном каталоге клиента. Никакой базы не нужно — история и есть поток строк, а список диалогов вообще кэшировать не надо так, как историю. Ниже — почему, и что для этого уже есть в библиотеке.
Всё сверено с исходниками 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.
Что из этого следует, и это надо решить: телефон и десктоп разойдутся. Создал группу «Работа» на десктопе — на телефоне её нет. Отсюда два пути:
- Группы только на устройстве. Просто, но раскладка не ездит между устройствами. Для одного человека за одним компьютером — нормально.
- Группы на сервере. Тогда нужно добавить в библиотеку хранение групп — это уже не «просто клиент», это расширение самого agentik.
Пока в макетах нарисован путь 1. Если хотим 2 — это отдельное решение, и его лучше принять до того, как начнём писать клиент.
4. Кэш истории — файл на диалог
Схема целиком описана в CACHE.md. Здесь — только где лежит.
Файл на диалог, одна строка на сообщение. Причина простая: диалоги друг с другом не связаны. Открыли один — читаем один файл, чужого не касаемся. Заодно один битый файл не портит остальные.
Как читаем: храним в памяти, на чём остановились (последняя прочитанная строка). При открытии диалога читаем конец файла — это последнее сообщение, с него и спрашиваем сервер «что новее». Весь файл при каждом открытии не перечитываем.
Картинки — отдельно. В сообщении картинка едет как массив байт. Держать
её внутри строки истории нельзя: файл распухнет, и станет нечитаемым.
Поэтому картинка кладётся в blobs/<id сообщения>, а в истории остаётся
ссылка на неё.
Три риска, честно
- Обрыв записи. Клиент упал посреди записи строки — в конце файла остался обрубок. Тогда: обрубок отбрасываем, спрашиваем сервер заново по последней целой строке. Так как сервер — источник правды, потеря не страшна.
- Очень длинный диалог. Файл растёт. Пока спасает то, что читаем конец, а не весь файл. Если однажды станет тяжело — режем историю на части по месяцам, но это потом, не сейчас.
- Файлов много. Да, на каждый диалог свой файл. Но файлы мелкие, а система умеет держать миллионы файлов. Проблемой это станет на порядки позже, чем что-то другое.
5. Список диалогов — кэшировать не надо (в главном)
Вот тут отличие от истории, и оно важное.
| История диалога | Список диалогов | |
|---|---|---|
| Сколько данных | много сообщений, каждое с текстом | сто строк, в каждой имя и дата |
| Размер одной порции | сотни килобайт | несколько килобайт |
| Растёт | на каждом ответе агента | медленно |
Вывод: список диалогов надо честно спрашивать у сервера. Причины:
- Он лёгкий. Сто диалогов — это несколько килобайт. Одна быстрая просьба, а не выкачивание истории.
- Он всегда свежий. Список меняется от чужой работы агента (он может вести другой диалог, пока мы смотрим этот). Кэшировать его и синхронизировать приростом — это сложная механика ради нескольких килобайт.
- Много диалогов — уже решено. В библиотеке есть постраничная выдача: берёт по 100 штук и подгружает следующие страницы по мере надобности. Отдельно ничего придумывать не надо.
Но пустое окно при запуске — реальная беда
Проблема не в размере списка, а в том, что пока он едет, окно пустое.
Поэтому в карте выше и лежит dialogs.json — снимок списка с прошлого раза.
Схема такая:
- Запустились — рисуем список из снимка. Мгновенно, окно не пустое.
- Одновременно спрашиваем сервер свежий список.
- Пришёл — заменяем нарисованное целиком.
Это не кэш, а занавеска, чтобы не смотреть в пустоту. Никакой сверки прироста, никаких сложных правил: пришёл свежий список — взяли его целиком. Снимок всегда считается устаревшим.
Если снимка нет (первый запуск) — показываем «загружаю».
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
Минимум, который закрывает макеты:
- Превью последнего сообщения в ответе списка диалогов.
- Число непрочитанных в ответе списка диалогов.
Обе правки — в том же хранилище, рядом с тем, что уже читается для списка. Ни новых таблиц, ни новых запросов со стороны клиента: как был один запрос за списком, так и остался.
Не обязательно: событие «в диалоге что-то произошло» — вместо него опрос раз в несколько секунд. Можно отложить.
Нужно решить отдельно: группы (§3). Это не правка ответа, это новое понятие в библиотеке.
7. Открытые вопросы
- Группы — на устройстве или на сервере? От этого зависит, поедут ли они между десктопом и телефоном (§3).
- Меняем API: превью и число непрочитанных? Или убираем их из макета и живём на том, что есть (§6.4).
- Что засчитывать сообщением при подсчёте непрочитанных: только ответы агента или ещё вызовы инструментов, которые в истории тоже лежат отдельными записями.
- Бейдж — число или точка? Точка не требует менять сервер вообще (§6.2).