# Где что хранится на диске **Вопрос:** где живут настройки, кэш истории и список диалогов. **Ответ коротко:** всё лежит в одном каталоге клиента. Никакой базы не нужно — история и есть поток строк, а список диалогов вообще кэшировать не надо так, как историю. Ниже — почему, и что для этого уже есть в библиотеке. Всё сверено с исходниками `agentik`, не по памяти. --- ## 1. Карта: что где лежит ``` ~/.agentik/ каталог клиента (на Windows — соответствующий системный) settings.json настройки: агенты, адреса, тема, выбранная группа groups.json группы и в какой группе какой диалог dialogs.json снимок списка диалогов — чтобы окно не было пустым read.json докуда дочитан каждый диалог (для счётчиков) history/ .jsonl сообщения: одна строка — одно сообщение blobs/ картинки из сообщений, отдельными файлами ``` Один каталог — простое правило: **удалил каталог, клиент чистый.** Ничего не прячется в других местах. ### Почему файлы, а не база История диалога — **только дописывается**: сообщений не правят и не удаляют (в библиотеке так и написано: «Только `insert` и чтение. Никаких обновлений»). Для дописываемого потока база не нужна: - **Строка на сообщение.** Читаем конец файла — знаем последнее сообщение. Дописываем в конец — вот и весь кэш. - **Понятно человеку.** Открыл файл — увидел сообщения. В случае беды можно посмотреть глазами и починить руками. - **Нет лишней зависимости.** База — это драйвер, версии, миграции схемы. Плюс это прямо соответствует тому, ради чего мы вообще выбрали десктоп эталоном: код, который **можно прочитать и понять**. --- ## 2. Настройки — один файл Пара агентов, адреса, тема, выбранная группа. Это десяток полей — им не нужна база и не нужен отдельный файл на каждую настройку. Важно: **настройки человек правит руками.** Если клиент однажды не запустится из-за кривого адреса, должно быть можно открыть `settings.json`, увидеть и исправить. Поэтому формат — обычный читаемый, а не сжатый. Отдельно: **секреты не в этом файле.** Если у агента будет пароль/ключ, он кладётся в системное хранилище паролей, а в настройках остаётся только ссылка на него. Иначе ключ утечёт вместе с настройками, которые человек может кому-то переслать. --- ## 3. Группы — они существуют только у нас **Это важное. Сервер про группы ничего не знает.** В библиотеке диалог описывается шестью полями: `id`, `title`, `isTemporal`, `createdAt`, `updatedAt` и два признака про картинки. **Поля «группа» там нет.** Значит, группы — целиком наша выдумка на стороне клиента, и жить они будут в `groups.json`. **Что из этого следует, и это надо решить:** телефон и десктоп **разойдутся**. Создал группу «Работа» на десктопе — на телефоне её нет. Отсюда два пути: 1. **Группы только на устройстве.** Просто, но раскладка не ездит между устройствами. Для одного человека за одним компьютером — нормально. 2. **Группы на сервере.** Тогда нужно добавить в библиотеку хранение групп — это уже не «просто клиент», это расширение самого agentik. Пока в макетах нарисован путь 1. Если хотим 2 — это отдельное решение, и его лучше принять **до** того, как начнём писать клиент. --- ## 4. Кэш истории — файл на диалог Схема целиком описана в `CACHE.md`. Здесь — только где лежит. **Файл на диалог, одна строка на сообщение.** Причина простая: диалоги друг с другом не связаны. Открыли один — читаем один файл, чужого не касаемся. Заодно один битый файл не портит остальные. **Как читаем:** храним в памяти, на чём остановились (последняя прочитанная строка). При открытии диалога читаем конец файла — это последнее сообщение, с него и спрашиваем сервер «что новее». Весь файл при каждом открытии не перечитываем. **Картинки — отдельно.** В сообщении картинка едет как массив байт. Держать её внутри строки истории нельзя: файл распухнет, и станет нечитаемым. Поэтому картинка кладётся в `blobs/`, а в истории остаётся ссылка на неё. ### Три риска, честно 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).