STORAGE.md переписан: хранилище — абстракция, реализация SQLite, настройки JSON; не файлы

This commit is contained in:
Porfiry
2026-09-19 16:44:54 +03:00
parent bd0f619585
commit 093115c415
4 changed files with 270 additions and 203 deletions
+21 -3
View File
@@ -84,6 +84,12 @@ fun events(after: Instant): Flow<Event>
**Самого кэша.** Хранилища на стороне клиента библиотека не даёт и не предлагает. **Самого кэша.** Хранилища на стороне клиента библиотека не даёт и не предлагает.
Это пишем мы. Библиотека умеет только «спроси, что новее» и «подпишись с момента». Это пишем мы. Библиотека умеет только «спроси, что новее» и «подпишись с момента».
Заодно **нет и абстракции хранилища** — ни интерфейса «дай сообщения диалога»,
ни готовой реализации под клиента. И то и другое наше, см. `STORAGE.md`.
Полезное: в самом `agentik` хранилище построено на **SQLDelight 2.3.2** — это
SQLite, умеющий и JVM, и Android. Тот же подход берём и мы, а не выдумываем.
--- ---
## 3. Где схема может порваться ## 3. Где схема может порваться
@@ -127,9 +133,20 @@ fun events(after: Instant): Flow<Event>
--- ---
## 4. Что ещё нужно решить (не решено) ## 4. Где хранить — РЕШЕНО
**Хранилище — абстракция, реализация — SQLite.** Подробно: `STORAGE.md`.
Клиент просит «дай сообщения диалога» и не знает, где они лежат. Реализацию
можно будет заменить (это понадобится на Android) — экраны не тронутся.
Ни одного упоминания SQLite вне реализации: если в экране встретилось
`sqlite` / `SQL` / `query` — абстракция прохудилась.
База — один файл в каталоге клиента. Настройки — **отдельно, JSON**:
их правит человек руками, в базу для этого лазить не должно быть нужно.
Осталось решить:
- **Где хранить кэш** — обычный файл на диске или лёгкая база.
- **Сколько держать** и когда чистить старые диалоги. - **Сколько держать** и когда чистить старые диалоги.
- **Что делать с прерванным ответом** в кэше (см. 3.2). - **Что делать с прерванным ответом** в кэше (см. 3.2).
- Связывать ли кэш с диалогом по `id` диалога — он стабилен, так что связка простая. - Связывать ли кэш с диалогом по `id` диалога — он стабилен, так что связка простая.
@@ -169,7 +186,8 @@ fun events(after: Instant): Flow<Event>
## 6. Порядок действий (предложение) ## 6. Порядок действий (предложение)
1. Определиться, где хранится кэш (раздел 4). 1. Описать абстракцию хранилища — интерфейсы (`STORAGE.md`, §1). Где лежит —
решено: SQLite, абстракция сверху.
2. Определиться с прерванным ответом (3.2) — иначе схема даст сбой на первом же «Стоп». 2. Определиться с прерванным ответом (3.2) — иначе схема даст сбой на первом же «Стоп».
3. При открытии диалога: кэш → спросить «новее» → догрузить → нарисовать. 3. При открытии диалога: кэш → спросить «новее» → догрузить → нарисовать.
4. Подписка на живой поток — тоже «с момента». 4. Подписка на живой поток — тоже «с момента».
+3 -2
View File
@@ -35,8 +35,9 @@ sketches/005-new-chat-picker/index.html # черновик модалки
а что не берём (архитектура, экраны, дизайн — своё). а что не берём (архитектура, экраны, дизайн — своё).
`MIC-ASR-SEARCH.md` — библиотеки для микрофона и распознавания: своё готовое `MIC-ASR-SEARCH.md` — библиотеки для микрофона и распознавания: своё готовое
(`mic-kmp`, `asr-kmp`, `vad-kmp`), вместо копирования кода из assistent. (`mic-kmp`, `asr-kmp`, `vad-kmp`), вместо копирования кода из assistent.
`STORAGE.md` — где что хранится: настройки, группы, кэш истории, список `STORAGE.md` — где что хранится: **хранилище делаем абстракцией, реализация —
диалогов; почему файлы, а не база; что менять в API (превью и число непрочитанных). SQLite, настройки — JSON.** Три отдельные абстракции (сообщения / настройки /
снимок списка), почему SQLite, что менять в API (превью и число непрочитанных).
`CACHE.md` — кэш сообщений на клиенте: что для него есть в библиотеке, схема `CACHE.md` — кэш сообщений на клиенте: что для него есть в библиотеке, схема
загрузки «только новое», где может порваться. загрузки «только новое», где может порваться.
`THEMES.md` — цветовые темы: цвета не пишем в коде, берём из схемы. Тем будет `THEMES.md` — цветовые темы: цвета не пишем в коде, берём из схемы. Тем будет
+28 -4
View File
@@ -267,8 +267,25 @@
- **R40.** **Живой поток тоже надо просить «с этого момента».** У подписки на события - **R40.** **Живой поток тоже надо просить «с этого момента».** У подписки на события
есть тот же параметр «после»; без него после переподключения пропустим события. есть тот же параметр «после»; без него после переподключения пропустим события.
- **R41.** **Решение:** кэш делаем. Схема — из R35. Где хранится (файл или лёгкая - **R41.** **Решение:** кэш делаем. Схема — из R35.
база) — **не решено**, см. раздел 9. - **R41.1.** **Хранилище — абстракция.** Клиент просит «дай сообщения диалога»,
«сохрани настройки» — и не знает, где это лежит. Реализацию можно заменить
(понадобится на Android) — экраны не тронутся. Подробно: `STORAGE.md`.
- **R41.2.** **Ни одного упоминания базы вне реализации.** Если в экране или
в логике клиента встретилось `sqlite` / `SQL` / `query` — абстракция
прохудилась. Тот же признак, что и с цветами числом в коде.
- **R41.3.** **Реализация — SQLite**, тот же подход, что в `agentik`:
**SQLDelight 2.3.2** (умеет и JVM, и Android). **Не файлы.**
- **R41.4.** **Три отдельные абстракции, не одна на всё:** сообщения диалога;
настройки; снимок списка диалогов. Разная жизнь — разными интерфейсами.
- **R41.5.** **Настройки — JSON-файл, не в базе.** Их правит человек руками:
сломался адрес агента — открыл, увидел, исправил. С базой для этого нужен
инструмент. Там же тема и выбранная группа.
- **R41.6.** **Секреты в JSON не пишем.** Пароль/ключ — в системное хранилище
паролей, в настройках только ссылка.
- **R41.7.** **Картинки — файлами, не в базе.** В сообщении картинка едет
массивом байт; держать её в базе нельзя — база распухнет и станет тяжёлой
для копирования. В базе — ссылка на файл.
### 8.3. Что не выносим ### 8.3. Что не выносим
@@ -283,11 +300,18 @@
- Нужны ли вложения (картинки) в этом клиенте. - Нужны ли вложения (картинки) в этом клиенте.
- Как именно различать агентов (см. R21–R22): цвета или картинки достаточно? - Как именно различать агентов (см. R21–R22): цвета или картинки достаточно?
- Нужен ли картинке размер/форма — показывать кружком без изменений или обрезать. - Нужен ли картинке размер/форма — показывать кружком без изменений или обрезать.
- Где хранится список агентов — в файле на диске или спрашивать сервер. - Где хранится список агентов — в контроле настроек (JSON) или спрашивать сервер.
- Как выглядит показ хода работы агента в свёрнутом виде. - Как выглядит показ хода работы агента в свёрнутом виде.
- **Где хранится кэш сообщений** — обычный файл на диске или лёгкая база (R41).
- Сколько держать в кэше и когда чистить (старые диалоги). - Сколько держать в кэше и когда чистить (старые диалоги).
- Что делать с куском прерванного ответа в кэше (R39): хранить или выбрасывать. - Что делать с куском прерванного ответа в кэше (R39): хранить или выбрасывать.
- **Группы — только на устройстве или на сервере?** Сервер про них не знает
(`STORAGE.md`, §7). Если только у нас — раскладка не поедет между десктопом
и телефоном.
- **Меняем ли API: превью последнего сообщения и число непрочитанных.**
Взять неоткуда, в макете они нарисованы (`STORAGE.md`, §6).
- **Бейдж — число или точка.** Точка сервер не трогает вообще (`STORAGE.md`, §6.2).
- **Где каталог клиента** — `~/.agentik/` или системный («Документы»).
На Android понятие «домашний каталог» своё.
## 10. Как проверяем ## 10. Как проверяем
+218 -194
View File
@@ -2,249 +2,273 @@
**Вопрос:** где живут настройки, кэш истории и список диалогов. **Вопрос:** где живут настройки, кэш истории и список диалогов.
**Ответ коротко:** всё лежит в одном каталоге клиента. Никакой базы не нужно — **Ответ коротко:** хранилище — **абстракция**. Клиент просит «дай сообщения
история и есть поток строк, а список диалогов вообще кэшировать не надо так, диалога», «сохрани настройки» — и не знает, где это лежит. Реализация —
как историю. Ниже — почему, и что для этого уже есть в библиотеке. **SQLite**. Настройки — **JSON**, потому что их правит человек.
Всё сверено с исходниками `agentik`, не по памяти. Абстракция нужна не ради красоты: **потом будет Android**, и там то же самое
хранилище надо будет собрать на другой основе. Если клиент всюду дёргает SQLite
напрямую, на Android придётся переписывать экраны. Если дёргает абстракцию —
меняется одна реализация, экраны не трогаются.
--- ---
## 1. Карта: что где лежит ## 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. Список диалогов — кэшировать не надо (в главном)
Вот тут отличие от истории, и оно важное.
| | История диалога | Список диалогов |
|---|---|---| |---|---|---|
| Сколько данных | много сообщений, каждое с текстом | сто строк, в каждой имя и дата | | Сколько | тысячи, растёт постоянно | десяток полей |
| Размер одной порции | сотни килобайт | несколько килобайт | | Меняются | только дописываются | переписываются целиком |
| Растёт | на каждом ответе агента | медленно | | Кто читает | приложение | приложение и **человек руками** |
| Поиск/выборка | нужны (по диалогу, по дате) | не нужен |
**Вывод: список диалогов надо честно спрашивать у сервера.** Причины: Это **две разные абстракции**, и живут они раздельно:
- **Он лёгкий.** Сто диалогов — это несколько килобайт. Одна быстрая просьба, ```kotlin
а не выкачивание истории. /** Сообщения диалогов: дописываем и читаем. Где лежит — не дело клиента. */
- **Он всегда свежий.** Список меняется от чужой работы агента (он может вести interface MessageRepository {
другой диалог, пока мы смотрим этот). Кэшировать его и синхронизировать suspend fun append(conversationId: String, message: CachedMessage)
приростом — это сложная механика ради нескольких килобайт. suspend fun read(conversationId: String, after: Instant, limit: Int): List<CachedMessage>
- **Много диалогов — уже решено.** В библиотеке есть постраничная выдача: suspend fun latest(conversationId: String): CachedMessage?
берёт по 100 штук и подгружает следующие страницы по мере надобности. suspend fun drop(conversationId: String)
Отдельно ничего придумывать не надо. }
### Но пустое окно при запуске — реальная беда /** Настройки: прочитать целиком, записать целиком. */
interface SettingsRepository {
suspend fun load(): Settings
suspend fun save(settings: Settings)
}
```
Проблема не в размере списка, а в том, что **пока он едет, окно пустое.** И **третья**, для списка диалогов:
Поэтому в карте выше и лежит `dialogs.json` — **снимок** списка с прошлого раза.
Схема такая: ```kotlin
/** Снимок списка диалогов — занавеска, чтобы окно не было пустым при запуске. */
interface ConversationListSnapshotRepository {
suspend fun load(): List<ConversationSummary>
suspend fun save(list: List<ConversationSummary>)
}
```
1. Запустились — рисуем список из снимка. Мгновенно, окно не пустое. Почему три, а не одна «на всё»:
2. Одновременно спрашиваем сервер свежий список.
3. Пришёл — заменяем нарисованное целиком.
Это **не кэш**, а занавеска, чтобы не смотреть в пустоту. Никакой сверки - **Сообщения и настройки — разная жизнь.** Сообщения дописываются и читаются
прироста, никаких сложных правил: пришёл свежий список — взяли его целиком. выборками, настройки переписываются целиком. Общий интерфейс заставит делать
Снимок всегда считается устаревшим. вид, что это одно и то же.
- **Разные реализации — норма.** Сообщения — SQLite. Настройки — JSON-файл.
Третье — тоже JSON. Под одним интерфейсом это выглядело бы как насилие.
- **Меньше знает — легче менять.** Экрану нужны сообщения — он видит только
сообщения. Как они лежат, его не касается.
Если снимка нет (первый запуск) — показываем «загружаю». ### Правило, по которому это проверяется
**Ни одного упоминания SQLite вне реализации.** Если в экране или в логике
клиента встретилось слово `sqlite`, `SQL`, `ResultSet`, `query` — абстракция
прохудилась. Это — тот самый признак, как с цветами: цвета числом в коде
не пишем, так и таблиц в экране не пишем.
---
## 2. Почему SQLite, а не файлы
**Решение: SQLite.**
- **То же самое будет на Android.** SQLite там родной. Одна реализация —
два устройства. Ради этого всё и затевается.
- **Не надо ничего придумывать.** Поиск, порядок, выборка «новее указанной
даты», отсечение дублей по `id` — это обычные запросы. С файлами каждое
такое место пришлось бы писать руками и потом отлаживать.
- **Запись не рассыпается.** Клиент упал посреди записи — база откатит
незавершённую сделку. С дописыванием строки в файл остаётся обрубок.
- **В библиотеке уже так.** В `agentik` хранилище построено на **SQLDelight
2.3.2** — это SQLite, умеющий и JVM, и Android. Не изобретаем: берём тот же
подход. (`storage-sqlite` в самом `agentik` — ровно это.)
### Что для этого уже есть
- **SQLDelight 2.3.2** — в библиотеке `agentik` уже подключён, с драйвером
и под JVM, и под Android.
- **Готовый образец запроса «новее»** — в `agentik` есть хранилище сообщений
на SQLite, где такой запрос уже написан. Повторяем приём, не выдумываем.
### Где лежит база
Один файл базы на клиента, в его каталоге:
```
<каталог клиента>/
client.db SQLite: сообщения, снимок списка диалогов, отметки
«докуда дочитано», раскладка по группам
settings.json настройки — их правит человек, поэтому JSON
blobs/ картинки из сообщений отдельными файлами
```
**Почему картинки не в базе:** в сообщении картинка едет как массив байт.
Хранить её в базе можно, но база от этого распухает и копировать её становится
тяжело. Поэтому картинка — файлом, а в базе только ссылка на файл.
---
## 3. Настройки — JSON
Единственное, что лежит не в базе. **Потому что их правит человек.**
Случилась беда, клиент не запускается из-за кривого адреса агента — открыл
`settings.json`, увидел, исправил. С базой так не получится: там чтобы
что-то поправить, нужен инструмент.
Там же — **тема** и **выбранная группа**: это тоже настройка, а не история
переписки.
**Секреты в этот файл не пишем.** Если у агента будет пароль или ключ,
он кладётся в системное хранилище паролей, а в настройках остаётся только
ссылка. Иначе ключ утечёт вместе с файлом, который человек может кому-то
переслать.
---
## 4. Сообщения диалога
Схема загрузки — в `CACHE.md`. Здесь только про хранение.
**Таблица сообщений.** Ключ — `id` сообщения, он стабилен: один и тот же
и в живом потоке, и в истории с сервера. Поэтому дубли отсекаются простой
проверкой, а не гаданием.
**Что кладём:** `id`, `id диалога`, дата, от кого, текст, признаки
(прервано/не закончено), ссылка на картинку из `blobs/`, если она есть.
**Что НЕ кладём:** сами байты картинок (см. §2).
**Отметка «докуда дочитано»** — отдельная мелочь на диалог. Из неё выходит
точка «есть новое»: сравнили дату последнего изменения диалога с отметкой —
и видно, появилось ли что-то. Цифра непрочитанных так не получится (см. §6),
только точка.
**Группы** — раскладка «какой диалог в какой группе». Сервер про группы ничего
не знает (см. §7), значит и это хранится только у нас.
---
## 5. Снимок списка диалогов — занавеска
Отдельный случай, и его надо понять правильно.
**Список диалогов кэшировать не надо.** Он лёгкий — сто строк по имени и дате
это несколько килобайт, а не история переписки. И он меняется от чужой работы:
пока мы смотрим один диалог, агент уже поработал в другом. Ловить тут прирост
сложнее, чем получить пользу.
**Но пустое окно при запуске — беда.** Пока список едет, смотреть не на что.
Поэтому мы **держим прошлый снимок списка** и показываем его сразу:
1. Запустились — нарисовали список из снимка. Мгновенно, окно не пустое.
2. Одновременно спросили у сервера свежий список.
3. Пришёл — заменили нарисованное **целиком**.
Это **не кэш** и никакой сверки прироста не требует. Снимок всегда считается
устаревшим, пришёл свежий — взяли его. Если снимка нет (первый запуск) —
показываем «загружаю».
**Лежит в той же базе** — отдельной таблицей. Мелочь вроде бы, но и она идёт
через абстракцию: экран просит «дай прошлый список», а не читает таблицу.
--- ---
## 6. Что в API не хватает ## 6. Что в API не хватает
Проверено по коду библиотеки. Вот что есть и чего нет для того, что Проверено по коду библиотеки. Что нужно для того, что нарисовано в макетах.
нарисовано в макетах.
| Что нужно в списке | Есть в библиотеке? | | Что нужно в списке | Есть? |
|---|---| |---|---|
| Название диалога | **Есть** — `title` | | Название диалога | **Есть** — `title` |
| Время последнего изменения | **Есть** — `updatedAt`, список уже отсортирован по свежести | | Время последнего изменения | **Есть** — `updatedAt`, список уже отсортирован по свежести |
| Понять, что диалог удалён или переименован | **Есть** — события `Deleted`, `Renamed` | | Понять, что диалог удалён или переименован | **Есть** — события `Deleted`, `Renamed` |
| Постраничная выдача | **Есть** — по 100 штук | | Постраничная выдача | **Есть** — по 100 штук |
| **Последнее сообщение строкой** | **Нет!** | | **Последнее сообщение строкой** | **Нет** |
| **Сколько непрочитанных** | **Нет!** | | **Число непрочитанных** | **Нет** |
| **Событие «в диалоге что-то произошло»** | **Нет!** | | **Событие «в диалоге что-то произошло»** | **Нет** |
| **Группа диалога** | **Нет!** | | **Группа диалога** | **Нет** |
Три дырки разберём отдельно — они разные по последствиям.
### 6.1. Превью последнего сообщения — нет ### 6.1. Превью последнего сообщения — нет
В макете в каждой строке под именем стоит текст: «Собрал отчёт, жду правок». В макете в строке списка под именем стоит текст «Собрал отчёт, жду правок».
**Взять его неоткуда.** В списке диалогов только имя и дата. **Взять его неоткуда** — в списке диалогов только имя и дата.
Варианты: - **Добавить в библиотеку — правильный путь.** В том же хранилище лежат
сообщения, превью берётся рядом со списком. Один запрос, как и был.
- Спрашивать по диалогу — сто диалогов, сто запросов. Не годится.
- Убрать из макета — строки станут суше, зато API не трогаем.
- **Добавить в библиотеку.** В том же хранилище лежат сообщения — превью ### 6.2. Число непрочитанных — нет
берётся одним запросом рядом со списком. Это правильный путь: один запрос
за списком, как и было.
- **Спрашивать по диалогу.** Сто диалогов — сто запросов. Не годится.
- **Убрать из макета.** Строки станут суше, зато ничего не меняем.
### 6.2. Счётчик непрочитанных — нет Кружок «2» в макете ничем не наполняется. Два уровня, разной цены:
В макете справа в строке стоит «2». **Числа взять неоткуда.** - **Точка «есть новое»** — сервер менять **не нужно**. Есть `updatedAt`
и наша отметка «докуда дочитано». Есть новое = дата изменения новее отметки.
Тут два уровня, и они разной цены:
- **Точка «есть новое»** — сервер менять **не нужно**. У нас уже есть
`updatedAt` (когда диалог последний раз менялся) и свой `read.json`
(докуда дочитали). Есть новое = `updatedAt` новее нашей отметки.
- **Число непрочитанных** — **нужен сервер.** Чтобы получить «2», надо знать, - **Число непрочитанных** — **нужен сервер.** Чтобы получить «2», надо знать,
сколько сообщений в диалоге всего: тогда число = всего минус прочитанное. сколько сообщений в диалоге всего. Одно поле в ответе списка.
Одно поле в ответе списка, считается по той же базе.
### 6.3. «В диалоге что-то произошло» — события нет ### 6.3. «В диалоге что-то произошло» — события нет
В потоке агента есть три события: **создан, удалён, переименован.** В потоке агента три события: **создан, удалён, переименован.** События «пришло
События «пришло новое сообщение» **нет** — хотя сервер в этот момент как раз новое сообщение» **нет** — хотя сервер в этот момент как раз обновляет дату
обновляет `updatedAt` диалога (это видно по коду: `touch(id, now)`). диалога (в коде это видно: `touch(id, now)`).
**Что это значит на практике:** агент работает в другом диалоге, а мы в этот **Что это значит:** агент работает в другом диалоге, а мы смотрим список —
момент смотрим список — **список сам не обновится.** Ни порядок не поедет, **список сам не обновится.** Ни порядок не поедет, ни точка не загорится,
ни точка «есть новое» не загорится, пока мы не спросим сервер заново. пока не спросим сервер.
Пути: - **Обновлять список самому, раз в несколько секунд** — без изменения API.
Список лёгкий, это честно и дёшево. Плюс обновлять при возвращении окна
в фокус.
- Добавить событие — список живёт сам, но опрос всё равно остаётся страховкой
от обрыва связи.
- **Обновлять список самому, раз в несколько секунд.** Без изменения API. Первого достаточно. Второе — приятная добавка на потом.
Список лёгкий (см. §5) — это честно и дёшево. Плюс обновлять при
возвращении окна в фокус.
- **Добавить событие.** Тогда список живёт сам, без опроса. Но опрос всё
равно остаётся как страховка от обрыва связи.
Первый путь достаточен. Второй — приятная добавка на потом.
### 6.4. Итог: что менять в API ### 6.4. Итог: что менять в API
**Минимум, который закрывает макеты:** **Минимум, закрывающий макеты:**
1. **Превью последнего сообщения** в ответе списка диалогов. 1. **Превью последнего сообщения** в ответе списка диалогов.
2. **Число непрочитанных** в ответе списка диалогов. 2. **Число непрочитанных** в ответе списка диалогов.
Обе правки — в том же хранилище, рядом с тем, что уже читается для списка. Обе — в том же хранилище, рядом с тем, что уже читается для списка. Новых
Ни новых таблиц, ни новых запросов со стороны клиента: как был один запрос запросов со стороны клиента не появляется.
за списком, так и остался.
**Не обязательно:** событие «в диалоге что-то произошло» — вместо него опрос **Не обязательно:** событие «в диалоге что-то произошло» — вместо него опрос.
раз в несколько секунд. Можно отложить. **Решить отдельно:** группы (§7).
**Нужно решить отдельно:** группы (§3). Это не правка ответа, это новое
понятие в библиотеке.
--- ---
## 7. Открытые вопросы ## 7. Группы — сервер про них не знает
1. **Группы — на устройстве или на сервере?** От этого зависит, поедут ли они **Важное.** В описании диалога шесть полей: `id`, `title`, `isTemporal`,
между десктопом и телефоном (§3). `createdAt`, `updatedAt` и два про картинки. **Поля «группа» нет.** Значит,
2. **Меняем API: превью и число непрочитанных?** Или убираем их из макета группы — целиком наша выдумка, и живут они только у нас.
и живём на том, что есть (§6.4).
3. **Что засчитывать сообщением** при подсчёте непрочитанных: только ответы **Что из этого следует:** телефон и десктоп **разойдутся**. Создал «Работа»
агента или ещё вызовы инструментов, которые в истории тоже лежат на компьютере — на телефоне её нет.
отдельными записями.
4. **Бейдж — число или точка?** Точка не требует менять сервер вообще (§6.2). 1. **Группы только на устройстве.** Просто. Раскладка не ездит между
устройствами.
2. **Группы на сервере.** Тогда это новое понятие в библиотеке `agentik`,
а не правка клиента.
Это решение стоит принять **до** того, как начнём писать клиент.
---
## 8. Открытые вопросы
1. **Группы — на устройстве или на сервере?** (§7)
2. **Меняем API: превью и число непрочитанных?** Или убираем их из макета (§6.4).
3. **Что считать сообщением** при подсчёте: только ответы агента или ещё
вызовы инструментов, которые в истории тоже лежат записями.
4. **Бейдж — число или точка?** Точка сервер не трогает вообще (§6.2).
5. **Где именно каталог клиента** — `~/.agentik/` или системный
(«Документы пользователя»). На Android понятие «домашний каталог» своё.