- chat.Router: ограниченный пул chatWorkers=4 воркеров + FIFO-очереди per-user (userState/workerLoop/runUser). Порядок сообщений одного UserID сохраняется; разные пользователи обрабатываются параллельно (до 4 одновременных LLM-вызовов), long-poll Telegram не блокируется чужим аналитиком. Backpressure по jobs — только на перегруженного пользователя. - app.FreeChat: sessions под sync.Mutex (защита от data race при параллельных воркерах роутера). - update: ResolveLatest проверяет наличие бинаря HEAD-пробой без скачивания тела (fallback GET Range 0-0 при 405/501), сортировка версий по id убыв.; один общий http.Client (keep-alive) вместо нового на каждый запрос. - тесты: порядок/параллелизм per-user в router, HEAD-без-тела и фоллбэк на версию без бинаря в update. - память Serena: инварианты Router/update, примечания по форматированию на Windows.
59 lines
8.2 KiB
Markdown
59 lines
8.2 KiB
Markdown
# core
|
||
|
||
Ratatoskr-go — оркестратор конвейера Ratatoskr (порт с Python на Go) в единый
|
||
бинарь. Субагенты (analyst/dev/reviewer) запускаются через внешний процесс
|
||
[opencode](https://opencode.ai) (v2 HTTP API). Интерфейсы: Telegram-бот + Fyne UI.
|
||
Linux — статический headless-бинарь (CGO_ENABLED=0); Windows — с Fyne-окном (cgo).
|
||
|
||
## Структура (модули internal/)
|
||
|
||
```
|
||
cmd/ratatoskr/ точка входа; флаги: -config, -version, -noui (headless). main.version/updateToken вшиваются ldflag'ом
|
||
internal/
|
||
app/ composition root/DI: App.New -> config.Load+Validate, ResolveExePaths, storage, Runner, Analyst, Core, Worker, Updater, UI. packageOwner="kamelion", Version="0.2.2"
|
||
config/ YAML+env загрузка (${VAR:-default}), defaults, validate C1-C4
|
||
model/ доменные типы/контракты: Status + validTransitions (IsValidTransition/IsTerminal), TraceStatus. Нижний слой — не зависит от storage/events
|
||
chat/ мультиканальный Router; telegram — long-poll канал. Коды M1-M5
|
||
core/ state-machine задач + Decider/analyst интерфейс. Коды D3/D4
|
||
analyst/ аналитик: промпт + разбор JSON-вердикта (opencode agent). Коды A1-A4
|
||
events/ шина событий UI: Bus (buffered Sub/Pub), типизированный Hub, события TaskCreated/TaskStatusChanged/HistoryAppended/TraceAppended/AgentActivity, LogBus (панель «Логи»)
|
||
worker/ polling-планировщик + dev/reviewer-конвейер + gitops. Коды W*, E*, R*
|
||
agents/ встроенные opencode-агенты (analyst.md, dev.md, reviewer.md) через go:embed
|
||
opencode/ HTTP-клиент v2 API opencode serve, поллинг вердикта, LiveRegistry. Коды O1-O5
|
||
storage/ SQLite (modernc.org/sqlite, без CGO): tasks, traces, task_history. Коды S1-S5
|
||
update/ автообновление из Gitea Packages. Коды U1-U6
|
||
ui/ desktop-оболочка: Store-абстракция (копии), Commands (UI->Core), Controller (шины->View), View/Window интерфейсы
|
||
ui/desktop/ Fyne-реализация окна (build-tag cgo, --noui / нет cgo → headless)
|
||
scripts/ build-publish-ui.ps1 — локальная Windows-сборка Fyne-бинаря + публикация в Gitea
|
||
docs/ ui-spec.md — спека Fyne UI (слои, event-bus, fyne.Do/снапшоты)
|
||
```
|
||
|
||
## Ключевые инварианты
|
||
|
||
- **App.New-сигнатура:** `App.New(configPath, version, updateToken string, noUI bool)` (4-й параметр — headless; cgo-вариант собирается только при наличии C-компилятора).
|
||
- **chat.Router:** асинхронная обработка входящих — **ограниченный пул `chatWorkers=4` воркеров + FIFO-очереди per-user** (`userState`, `workerLoop`/`runUser`). Порядок сообщений одного UserID сохраняется (флаг `scheduled` → один активный воркер на пользователя); разные пользователи обрабатываются параллельно (до 4 одновременных LLM-вызовов). Backpressure по `jobs` блокирует только перегруженного пользователя, не весь long-poll. `Processed()`/`WaitProcessed()` — синхронизация тестов.
|
||
- **FreeChat** (app): `sessions map[uid]sessionID` защищён `sync.Mutex` (пишется из разных воркеров роутера).
|
||
- **UI:** окно — ещё одна реализация `chat.Channel` (присоединяется в Router). Core не трогает UI; обмен — событийная шина (events). Кнопка «Завершить» = полный выход (SetOnQuit→cancel→UI.Run возвращается); закрытие крестиком = сворачивание, Core живёт. UI собирается с `--noui`/без cgo.
|
||
- **Фазы аналитика (Decision.Phase):** `ask`, `propose`, `ready` (два последних обрабатываются одинаково в core), `abort`. Требования валидатора: ask — chat_reply/questions; propose — хотя бы одно изменённое поле; ready — без изменённых полей.
|
||
- **Статусы задач (internal/model):** draft→collecting→ready→approved→running→success/failed/timeout + cancelled/aborted/closed (терминальные). UserID — chat.ID (одна активная задача на чат).
|
||
- **Decider/Worker/Analyst/Reviewer:** Decider=analyst интерфейс; Worker — polling-планировщик; Reviewer проверяет diff dev-ветки (R1-R6), вердикт JSON {passed, critical_issues, solid_violations, comments}.
|
||
- **gitops (worker):** worktree-режим; feature-ветка `feat/<taskTag>` от origin/main; push через http.extraHeader, токен Bearer.
|
||
- **Пути «всё рядом с .exe»:** db/worktree резолвятся от ExeDir; config.yaml — рядом с бинарём, фоллбэк cwd.
|
||
- **Автообновление:** авто = только Check+уведомление; замена — по /update; версии в `commit-<sha7>/` (не `latest/`); Verify сверяет контрольную сумму бинаря против `.sha256` той же версии. **Perf:** `ResolveLatest` проверяет наличие бинаря версии **HEAD-пробой без скачивания тела** (405/501 → fallback `GET Range: bytes=0-0`), сортировка версий по id убыв.; один общий `http.Client` (keep-alive). Ошибка U4 — только при несовпадении суммы (пустой/отсутствующий `.sha256` пропускает проверку — M1-известное замечание).
|
||
|
||
## opencode (v2 HTTP API, >= 1.18.18)
|
||
|
||
- Интеграция с субагентами — через headless `opencode serve`, **v2 API** (`/api/*`). Версия opencode >= 1.18.18.
|
||
- **Хардпин модели:** при создании сессии читается top-level `model` из конфига opencode (`internal/opencode/config.go`, JSONC-стрип) и передаётся в `POST /api/session` как `{"model":{providerID,id}}`.
|
||
- **О5 WARN (устойчивость к v1-конфигу):** конфиг по старой схеме молча игнорируется v2; провайдер без api → unsupported модели → fallback. Ratatoskr не чинит сам, но логирует warning; фактическая модель ответа сравнивается с ожидаемой. Правильный v2-вид: `api:{type:"aisdk",package,url}`, `request.headers` вместо `options.headers`.
|
||
- **Поллинг вердикта:** `POST /api/session/:id/prompt` (durable admit) → `GET /api/session/:id/message?order=desc&limit=200` (новые assistant-сообщения, текст в `content[].type=="text"`) → завершение = `GET /api/session/active` без сессии + финальное assistant-сообщение, стабильное `settlePolls=2` опроса. `POST .../interrupt` вместо abort.
|
||
|
||
## Контракты (не ломать)
|
||
|
||
- `App.New(configPath, version, updateToken string, noUI bool)` — сигнатура.
|
||
- `packageOwner` — константа "kamelion" (не плодить vars/ldflag/конфиг).
|
||
- `update.Updater` — создаётся структурой `&update.Updater{...}`, конструктора нет.
|
||
- `ui.Store` — чтение-модель для UI: ListTasks/GetTask/GetHistory/GetTraces (копии, без ссылок на storage).
|
||
- `events.Bus`/`events.LogBus` — шина UI (см. internal/events). publisher-интерфейсы подключаются к Core/Worker.
|
||
|
||
см. также `mem:tech_stack`, `mem:conventions`, `mem:task_completion`, `mem:suggested_commands`. |