Files
agentik-desktop/STORAGE.md
T

16 KiB
Raw Blame History

Где что хранится на диске

Вопрос: где живут настройки, кэш истории и список диалогов.

Ответ коротко: всё лежит в одном каталоге клиента. Никакой базы не нужно — история и есть поток строк, а список диалогов вообще кэшировать не надо так, как историю. Ниже — почему, и что для этого уже есть в библиотеке.

Всё сверено с исходниками 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).