diff --git a/CACHE.md b/CACHE.md index 18bac8b..8d4c2e0 100644 --- a/CACHE.md +++ b/CACHE.md @@ -84,6 +84,12 @@ fun events(after: Instant): Flow **Самого кэша.** Хранилища на стороне клиента библиотека не даёт и не предлагает. Это пишем мы. Библиотека умеет только «спроси, что новее» и «подпишись с момента». +Заодно **нет и абстракции хранилища** — ни интерфейса «дай сообщения диалога», +ни готовой реализации под клиента. И то и другое наше, см. `STORAGE.md`. + +Полезное: в самом `agentik` хранилище построено на **SQLDelight 2.3.2** — это +SQLite, умеющий и JVM, и Android. Тот же подход берём и мы, а не выдумываем. + --- ## 3. Где схема может порваться @@ -127,9 +133,20 @@ fun events(after: Instant): Flow --- -## 4. Что ещё нужно решить (не решено) +## 4. Где хранить — РЕШЕНО + +**Хранилище — абстракция, реализация — SQLite.** Подробно: `STORAGE.md`. + +Клиент просит «дай сообщения диалога» и не знает, где они лежат. Реализацию +можно будет заменить (это понадобится на Android) — экраны не тронутся. +Ни одного упоминания SQLite вне реализации: если в экране встретилось +`sqlite` / `SQL` / `query` — абстракция прохудилась. + +База — один файл в каталоге клиента. Настройки — **отдельно, JSON**: +их правит человек руками, в базу для этого лазить не должно быть нужно. + +Осталось решить: -- **Где хранить кэш** — обычный файл на диске или лёгкая база. - **Сколько держать** и когда чистить старые диалоги. - **Что делать с прерванным ответом** в кэше (см. 3.2). - Связывать ли кэш с диалогом по `id` диалога — он стабилен, так что связка простая. @@ -169,7 +186,8 @@ fun events(after: Instant): Flow ## 6. Порядок действий (предложение) -1. Определиться, где хранится кэш (раздел 4). +1. Описать абстракцию хранилища — интерфейсы (`STORAGE.md`, §1). Где лежит — + решено: SQLite, абстракция сверху. 2. Определиться с прерванным ответом (3.2) — иначе схема даст сбой на первом же «Стоп». 3. При открытии диалога: кэш → спросить «новее» → догрузить → нарисовать. 4. Подписка на живой поток — тоже «с момента». diff --git a/README.md b/README.md index d89d507..1d75fef 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,9 @@ sketches/005-new-chat-picker/index.html # черновик модалки а что не берём (архитектура, экраны, дизайн — своё). `MIC-ASR-SEARCH.md` — библиотеки для микрофона и распознавания: своё готовое (`mic-kmp`, `asr-kmp`, `vad-kmp`), вместо копирования кода из assistent. -`STORAGE.md` — где что хранится: настройки, группы, кэш истории, список -диалогов; почему файлы, а не база; что менять в API (превью и число непрочитанных). +`STORAGE.md` — где что хранится: **хранилище делаем абстракцией, реализация — +SQLite, настройки — JSON.** Три отдельные абстракции (сообщения / настройки / +снимок списка), почему SQLite, что менять в API (превью и число непрочитанных). `CACHE.md` — кэш сообщений на клиенте: что для него есть в библиотеке, схема загрузки «только новое», где может порваться. `THEMES.md` — цветовые темы: цвета не пишем в коде, берём из схемы. Тем будет diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 1cfd332..d897790 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -267,8 +267,25 @@ - **R40.** **Живой поток тоже надо просить «с этого момента».** У подписки на события есть тот же параметр «после»; без него после переподключения пропустим события. -- **R41.** **Решение:** кэш делаем. Схема — из R35. Где хранится (файл или лёгкая - база) — **не решено**, см. раздел 9. +- **R41.** **Решение:** кэш делаем. Схема — из R35. +- **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. Что не выносим @@ -283,11 +300,18 @@ - Нужны ли вложения (картинки) в этом клиенте. - Как именно различать агентов (см. R21–R22): цвета или картинки достаточно? - Нужен ли картинке размер/форма — показывать кружком без изменений или обрезать. -- Где хранится список агентов — в файле на диске или спрашивать сервер. +- Где хранится список агентов — в контроле настроек (JSON) или спрашивать сервер. - Как выглядит показ хода работы агента в свёрнутом виде. -- **Где хранится кэш сообщений** — обычный файл на диске или лёгкая база (R41). - Сколько держать в кэше и когда чистить (старые диалоги). - Что делать с куском прерванного ответа в кэше (R39): хранить или выбрасывать. +- **Группы — только на устройстве или на сервере?** Сервер про них не знает + (`STORAGE.md`, §7). Если только у нас — раскладка не поедет между десктопом + и телефоном. +- **Меняем ли API: превью последнего сообщения и число непрочитанных.** + Взять неоткуда, в макете они нарисованы (`STORAGE.md`, §6). +- **Бейдж — число или точка.** Точка сервер не трогает вообще (`STORAGE.md`, §6.2). +- **Где каталог клиента** — `~/.agentik/` или системный («Документы»). + На Android понятие «домашний каталог» своё. ## 10. Как проверяем diff --git a/STORAGE.md b/STORAGE.md index 8a0a500..8616bf2 100644 --- a/STORAGE.md +++ b/STORAGE.md @@ -2,249 +2,273 @@ **Вопрос:** где живут настройки, кэш истории и список диалогов. -**Ответ коротко:** всё лежит в одном каталоге клиента. Никакой базы не нужно — -история и есть поток строк, а список диалогов вообще кэшировать не надо так, -как историю. Ниже — почему, и что для этого уже есть в библиотеке. +**Ответ коротко:** хранилище — **абстракция**. Клиент просит «дай сообщения +диалога», «сохрани настройки» — и не знает, где это лежит. Реализация — +**SQLite**. Настройки — **JSON**, потому что их правит человек. -Всё сверено с исходниками `agentik`, не по памяти. +Абстракция нужна не ради красоты: **потом будет Android**, и там то же самое +хранилище надо будет собрать на другой основе. Если клиент всюду дёргает SQLite +напрямую, на Android придётся переписывать экраны. Если дёргает абстракцию — +меняется одна реализация, экраны не трогаются. --- -## 1. Карта: что где лежит +## 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 штук и подгружает следующие страницы по мере надобности. - Отдельно ничего придумывать не надо. +```kotlin +/** Сообщения диалогов: дописываем и читаем. Где лежит — не дело клиента. */ +interface MessageRepository { + suspend fun append(conversationId: String, message: CachedMessage) + suspend fun read(conversationId: String, after: Instant, limit: Int): List + suspend fun latest(conversationId: String): CachedMessage? + 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 + suspend fun save(list: List) +} +``` -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 не хватает -Проверено по коду библиотеки. Вот что есть и чего нет для того, что -нарисовано в макетах. +Проверено по коду библиотеки. Что нужно для того, что нарисовано в макетах. -| Что нужно в списке | Есть в библиотеке? | +| Что нужно в списке | Есть? | |---|---| | Название диалога | **Есть** — `title` | | Время последнего изменения | **Есть** — `updatedAt`, список уже отсортирован по свежести | | Понять, что диалог удалён или переименован | **Есть** — события `Deleted`, `Renamed` | | Постраничная выдача | **Есть** — по 100 штук | -| **Последнее сообщение строкой** | **Нет!** | -| **Сколько непрочитанных** | **Нет!** | -| **Событие «в диалоге что-то произошло»** | **Нет!** | -| **Группа диалога** | **Нет!** | - -Три дырки разберём отдельно — они разные по последствиям. +| **Последнее сообщение строкой** | **Нет** | +| **Число непрочитанных** | **Нет** | +| **Событие «в диалоге что-то произошло»** | **Нет** | +| **Группа диалога** | **Нет** | ### 6.1. Превью последнего сообщения — нет -В макете в каждой строке под именем стоит текст: «Собрал отчёт, жду правок». -**Взять его неоткуда.** В списке диалогов только имя и дата. +В макете в строке списка под именем стоит текст «Собрал отчёт, жду правок». +**Взять его неоткуда** — в списке диалогов только имя и дата. -Варианты: +- **Добавить в библиотеку — правильный путь.** В том же хранилище лежат + сообщения, превью берётся рядом со списком. Один запрос, как и был. +- Спрашивать по диалогу — сто диалогов, сто запросов. Не годится. +- Убрать из макета — строки станут суше, зато API не трогаем. -- **Добавить в библиотеку.** В том же хранилище лежат сообщения — превью - берётся одним запросом рядом со списком. Это правильный путь: один запрос - за списком, как и было. -- **Спрашивать по диалогу.** Сто диалогов — сто запросов. Не годится. -- **Убрать из макета.** Строки станут суше, зато ничего не меняем. +### 6.2. Число непрочитанных — нет -### 6.2. Счётчик непрочитанных — нет +Кружок «2» в макете ничем не наполняется. Два уровня, разной цены: -В макете справа в строке стоит «2». **Числа взять неоткуда.** - -Тут два уровня, и они разной цены: - -- **Точка «есть новое»** — сервер менять **не нужно**. У нас уже есть - `updatedAt` (когда диалог последний раз менялся) и свой `read.json` - (докуда дочитали). Есть новое = `updatedAt` новее нашей отметки. +- **Точка «есть новое»** — сервер менять **не нужно**. Есть `updatedAt` + и наша отметка «докуда дочитано». Есть новое = дата изменения новее отметки. - **Число непрочитанных** — **нужен сервер.** Чтобы получить «2», надо знать, - сколько сообщений в диалоге всего: тогда число = всего минус прочитанное. - Одно поле в ответе списка, считается по той же базе. + сколько сообщений в диалоге всего. Одно поле в ответе списка. ### 6.3. «В диалоге что-то произошло» — события нет -В потоке агента есть три события: **создан, удалён, переименован.** -События «пришло новое сообщение» **нет** — хотя сервер в этот момент как раз -обновляет `updatedAt` диалога (это видно по коду: `touch(id, now)`). +В потоке агента три события: **создан, удалён, переименован.** События «пришло +новое сообщение» **нет** — хотя сервер в этот момент как раз обновляет дату +диалога (в коде это видно: `touch(id, now)`). -**Что это значит на практике:** агент работает в другом диалоге, а мы в этот -момент смотрим список — **список сам не обновится.** Ни порядок не поедет, -ни точка «есть новое» не загорится, пока мы не спросим сервер заново. +**Что это значит:** агент работает в другом диалоге, а мы смотрим список — +**список сам не обновится.** Ни порядок не поедет, ни точка не загорится, +пока не спросим сервер. -Пути: +- **Обновлять список самому, раз в несколько секунд** — без изменения API. + Список лёгкий, это честно и дёшево. Плюс обновлять при возвращении окна + в фокус. +- Добавить событие — список живёт сам, но опрос всё равно остаётся страховкой + от обрыва связи. -- **Обновлять список самому, раз в несколько секунд.** Без изменения API. - Список лёгкий (см. §5) — это честно и дёшево. Плюс обновлять при - возвращении окна в фокус. -- **Добавить событие.** Тогда список живёт сам, без опроса. Но опрос всё - равно остаётся как страховка от обрыва связи. - -Первый путь достаточен. Второй — приятная добавка на потом. +Первого достаточно. Второе — приятная добавка на потом. ### 6.4. Итог: что менять в API -**Минимум, который закрывает макеты:** +**Минимум, закрывающий макеты:** 1. **Превью последнего сообщения** в ответе списка диалогов. 2. **Число непрочитанных** в ответе списка диалогов. -Обе правки — в том же хранилище, рядом с тем, что уже читается для списка. -Ни новых таблиц, ни новых запросов со стороны клиента: как был один запрос -за списком, так и остался. +Обе — в том же хранилище, рядом с тем, что уже читается для списка. Новых +запросов со стороны клиента не появляется. -**Не обязательно:** событие «в диалоге что-то произошло» — вместо него опрос -раз в несколько секунд. Можно отложить. - -**Нужно решить отдельно:** группы (§3). Это не правка ответа, это новое -понятие в библиотеке. +**Не обязательно:** событие «в диалоге что-то произошло» — вместо него опрос. +**Решить отдельно:** группы (§7). --- -## 7. Открытые вопросы +## 7. Группы — сервер про них не знает -1. **Группы — на устройстве или на сервере?** От этого зависит, поедут ли они - между десктопом и телефоном (§3). -2. **Меняем API: превью и число непрочитанных?** Или убираем их из макета - и живём на том, что есть (§6.4). -3. **Что засчитывать сообщением** при подсчёте непрочитанных: только ответы - агента или ещё вызовы инструментов, которые в истории тоже лежат - отдельными записями. -4. **Бейдж — число или точка?** Точка не требует менять сервер вообще (§6.2). +**Важное.** В описании диалога шесть полей: `id`, `title`, `isTemporal`, +`createdAt`, `updatedAt` и два про картинки. **Поля «группа» нет.** Значит, +группы — целиком наша выдумка, и живут они только у нас. + +**Что из этого следует:** телефон и десктоп **разойдутся**. Создал «Работа» +на компьютере — на телефоне её нет. + +1. **Группы только на устройстве.** Просто. Раскладка не ездит между + устройствами. +2. **Группы на сервере.** Тогда это новое понятие в библиотеке `agentik`, + а не правка клиента. + +Это решение стоит принять **до** того, как начнём писать клиент. + +--- + +## 8. Открытые вопросы + +1. **Группы — на устройстве или на сервере?** (§7) +2. **Меняем API: превью и число непрочитанных?** Или убираем их из макета (§6.4). +3. **Что считать сообщением** при подсчёте: только ответы агента или ещё + вызовы инструментов, которые в истории тоже лежат записями. +4. **Бейдж — число или точка?** Точка сервер не трогает вообще (§6.2). +5. **Где именно каталог клиента** — `~/.agentik/` или системный + («Документы пользователя»). На Android понятие «домашний каталог» своё.