После завершения задачи со статусом failed/timeout воркер запускает
постмортем-анализ (агент postmortem): разбирает сессии dev/reviewer,
оценивает причины сбоя и шлёт владельцу уведомление с анализом. Статус
задачи не меняет; сбои анализа не влияют на исход.
- 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.
- проверка gcc перенесена после чтения creds (шаг 1a)
- GCC_PATH дописывается в PATH (абсолютный или относительно scripts/);
без него — gcc из текущего PATH
- комментарии в шапку: GCC_PATH и уточнённый PROJECT_PATH
- LogBuffer: ленивое вытеснение через start + инкрементальный total убирает
O(n^2) копирование entries[1:] при большом числе записей
- LogPanel: лимит 10MB (maxLogBytes), перерисовка ленты коалесцируется
(200мс) — строка не пересобирается на каждый append
- build-publish-ui.ps1: корень проекта задаётся через PROJECT_PATH в
.gitea-creds (относительно scripts/), без него — папка рядом со скриптом
- тесты: живые записи Entries/Render, кап по байтам
- ui.Session (UserID/Address/Title/BoundTask) + фабрики NewFreeSession/NewTaskSession и префикс ui://sess-*
- свободная вкладка без задачи: свой UserID, диалог с LLM в памяти (agent=chat, resume по sessionID)
- /start в свободной вкладке создаёт задачу с ChatID == UserID вкладки -> вкладка становится task-сессией
- desktop окно: container.AppTabs с вкладками, кнопка «Новый чат», маршрутизация Send/Ask/истории по Address вкладки
- app.FreeChat: in-memory связка uid -> opencode sessionID; роутинг свободного чата в handleIncoming
- агент chat.md + регистрация 'chat' в agents.Names
- тесты: session_test.go (фабрики/хелперы)
Спеца 12.12: интерфейс + Nil + FormatRole в internal/ui/chat_panel.go,
Fyne-реализация в internal/ui/desktop/chat_panel.go (транскрипт + композитор).
Окно как chat.Channel рендерит Send/Ask/историю через Append; state_panel
не трогаем.
Спеца 12.11: интерфейс + Nil + TaskDetailText в internal/ui/task_detail.go,
Fyne-реализация в internal/ui/desktop/task_detail_panel.go. selectTask
делегирует в панель; диалог и «Состояние» — отдельные панели, не трогаем.
Спеца 12.10: интерфейс + Nil + TaskTitle в internal/ui/task_list_panel.go,
Fyne-реализация в internal/ui/desktop/task_list_panel.go. Состояние выбора
живёт в окне; выбранная строка подсвечивается (Select/Unselect).
Спеца 12.8-12.9: интерфейс + Nil в internal/ui/log_panel.go, Fyne-реализация
в internal/ui/desktop/log_panel.go, окно использует контракт вместо logsLbl.
Команды (спец 12.2):
- ui.Commands: действия UI → текстовые команды канала (start/cancel/skip/
retry/continue/approve/send), единый путь через chat.Channel.
- ui.Window = chat.Channel + View; NilWindow для headless/тестов.
Snapshots (спец 12.5):
- ui.Store: чтение-модель, возвращает только копии (ListTasks/GetTask/
GetHistory/GetTraces); ui.DBStore поверх storage.
Fyne-окно (internal/ui/desktop, build-tag cgo):
- список задач слева, сплиты рабочей области и панели «Логи»/«Состояние»,
ввод+кнопки команд, тёмная тема, fullscreen, сохранение layout в
Preferences, сворачивание при закрытии крестиком.
- Колбэки View через fyne.Do (спец 12.4).
Интеграция:
- app.New(..., noUI); флаг --noui; UI собирается только с cgo (ui_cgo/
ui_noui фабрики), приложение headless без него.
- Run: окно блокирует главную горутину; «Завершить» → cancel → graceful
shutdown (спец 12.6).
- go.mod: fyne.io/fyne/v2 v2.6.0 (direct).
- View — интерфейс Fyne-слоя: колбэки на все доменные события + логи.
- Controller подписывается на доменную и логовую шины (через events.Hub)
и диспатчит события в View; колбэки выполняются в горутинах Hub, поэтому
реализация Fyne обязана обновлять виджеты через fyne.Do (спец 12.4).
- Close отписывает контроллер (сворачивание окна) без остановки Core.
- NilView — no-op для headless (--noui) и тестов.
- Юнит-тесты: доставка доменных/логовых событий, остановка после Close.
- Hub слушает *Bus в собственной горутине и вызывает зарегистрированные
обработчики по типу события (On[T]); порядок сохраняется.
- Мост к UI: колбэки выполняются в горутине Hub → внутри можно переложить
работу на поток Fyne (fyne.Do) или thread-safe binding.
- Close останавливает горутину и отписывается; Start идемпотентен.
- Юнит-тесты: доставка по типу, игнор посторонних типов, несколько
обработчиков одного типа, остановка после Close.
- internal/model: единый источник статусов задачи (Status, TraceStatus,
машина переходов), без зависимости от storage.
- internal/storage: совместимый мост (type Status = model.Status,
re-export констант) — внешний код не меняется.
- internal/events: шина событий (fan-out, блокирующий Publish с гарантией
порядка), события задач/трейсов, отдельная логовая шина + LogWriter,
Publisher/NilPublisher для внедрения в Core/Worker.
- docs/ui-spec.md: спецификация десктопного UI (Fyne).
Растущий в один text-парт стрим (text-delta) и reasoning больше не
выглядят как зависшая нейронка: idle-таймер сбрасывается по росту
числа партов и суммарной длины text/reasoning.
- E2E (app): e2eFakeAPI переведён на v2 HTTP API opencode (/api/*) с
определением агента по тексту промпта; Router получает processed-счётчик
и WaitProcessed, e2eChannel.deliver ждёт асинхронную обработку — убирает
гонку «запрос сразу после deliver» и коллатеральный 'database is closed'.
- app_test: одинарные YAML-кавычки для путей Windows (backslash-escape) +
закрытие Store в TestNew/TestNew_RunCtxCancel/TestNew_UpdateWiring.
- config_test: абсолютный путь строится с корнем тома (C:\...) и одинарными
кавычками YAML.
- opencode/server_test: fakeServeBin на Windows — .cmd с ping (#!/bin/sh
не исполняется).
- worker_test: TestWorkerSemaphore поллит до целевого статуса вместо
фиксированных sleep (git на Windows медленнее).
Корень проблемы «не получаем результаты»: клиент смешивал два слоя opencode
serve. CreateSession ходил на /api/session (v2, ждал {data.id}), Verdict — на
/api/session/{id}/message?order=desc и ждал {data:[{type,content}]}, где поле
content[].type/text физически отсутствует, поэтому вердикт никогда не находился
и поллинг уходил в вечный таймаут. Abort и вовсе звал несуществующий /interrupt.
Теперь весь код на experimental-слое, как сверено с sst/opencode (ветка dev):
- CreateSession: POST /session → голая Session, id в .id.
- Send: блокирующий POST /session/{id}/message, тело {parts:[{type:text,text}]},
вердикт из частей parts[].type=="text" ответа. Это и есть результат — метод
Verdict и отдельный GET удалены.
- textCount (прогресс): GET /session/{id}/message → голый массив [{info, parts}].
- Abort: POST /session/{id}/abort.
Runner: блокирующий Send запускается в горутине (канал вердикта/ошибки),
параллельно поллим textCount (рост text-частей сбрасывает idle-таймер). При
idle/hard-таймауте или отмене контекста — Abort + cancel() Send-горутины → rc=-1.
Send ходит через отдельный http.Client без жёсткого Timeout (управляется ctx),
чтобы длинная генерация не обрывалась на 30s. Тесты/fakeAPIServer переведены на
экспериментальный формат. Версия → 0.2.2.
Router теперь обрабатывает входящие в воркер-горутине (FIFO-очередь с
буфером 256) вместо синхронного вызова onUserMsg из long-poll цикла
канала. Долгий вызов аналитика (Decide) больше не блокирует приём
новых сообщений от Telegram: цикл getUpdates продолжает работать.
- router.go: NewRouter запускает processLoop; handleIncoming кладёт
событие в канал и возвращается; маршрутизация + pending по-прежнему
обновляются синхронно под мьютексом.
- router_test.go: fakeOnMsg стал потокобезопасным с ожиданием числа
входящих (wait), т.к. обработка теперь асинхронная.
Преимущества: интерфейс не замирает на время анализа; порядок входящих
сохраняется (FIFO). Ограничение: воркер один — при очень долгом аналитике
следующие сообщения ждут в очереди, но канал их продолжает принимать.
Убран префикс /api из POST-запроса на отправку сообщения, чтобы
совпадать с фактическим роутом serve v1.18. GET чтения сообщений
остаётся на /api/session/:id/message.
- client.go: Send → POST /session/:id/message
- тесты: fakeAPIServer отвечает на POST /session/{id}/message (GET — /api)
Промпт отправляется в /api/session/:id/message (v1.18) с новым форматом
тела parts:[{type:"text"}], вместо {prompt:{text}}. Причина перехода:
/prompt сам подставляет не ту модель, что в конфиге; /message использует
модель из конфига.
- client.go: Send → POST /message + тело parts; шапка-док v1.18
- тесты: fakeAPIServer + e2eFakeAPI отвечают на POST /message (GET уже был)
В единой точке Client.do добавлены отладочные логи (всегда включены):
- строка вызова: opencode api <op> -> <METHOD> <url><path>
- тело запроса (усечено до 5000 байт)
- тело ответа 2xx (усечено до 5000 байт)
- тело ответа при ошибке/не-2xx (усечено до 1000 байт)
Нужно для диагностики проблемы аналитика (serve отвечал на модель
opencode/nemotron, ratatoskr не видел вердикт).
procgroup_unix.go имеет unix build-tag; под windows setpgid/killGroup не
определялись → GOOS=windows сборка падала. Добавлен procgroup_windows.go:
setpgid — no-op (нет Setpgid), killGroup — Process.Kill (дочерние группы не
завершаются).
Runner теперь ходит к постоянным serve по HTTP API (v1.17+, /api):
- клиент Client (create/send/wait/abort/messages/verdict)
- Pool: по одному serve на каталог, ленивый подъём, root-сервер в worktree,
выделение портов, ReleaseTask при завершении задачи
- Run: CreateSession('ratatoskr-<агент>') -> Send -> поллинг Verdict из
text-частей assistant-сообщений; idle/hard таймауты дают RC=-1
- вердикт извлекается из последнего assistant text-парта (плоский text)
- тесты: unit на фейковом HTTP-сервере; e2e эмулирует serve через httptest,
агент определяется по title сессии
Супервайзер Server spawn'ит opencode serve (или ходит на внешний URL),
Runner ходит к нему через 'opencode run --attach <url>'. По умолчанию
serve.enabled=false — историческая spawn-модель сохранена; наличие
serve.url переключает на внешний сервер.
- config: ServeCfg (enabled/hostname/port/url/password) + дефолты; лоадер
научился int/bool (раньше только string/Duration/struct).
- opencode: Server (Start/Run/Close, /global/health, рестарт упавшего,
reaper-горутина владеет Wait; .Exited() непригоден для SIGKILL).
- Runner.AttachURL: run --attach <url> при заданном URL, иначе как раньше.
- app: composition root — при включённом serve запускает супервайзер.
- тесты: TestRun_AttachMode, TestServer_{ExternalURL,OwnProcess,Restart}.