feat: opencode через HTTP API — пул serve-серверов вместо spawn/NDJSON
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 сессии
This commit is contained in:
@@ -60,7 +60,7 @@ type App struct {
|
||||
Worker *worker.Worker
|
||||
Updater *update.Updater
|
||||
tg *telegram.Channel // сохранена для Run
|
||||
serve *opencode.Server // супервайзер opencode serve (режим --attach); nil, если выключен
|
||||
pool *opencode.Pool // пул opencode serve-серверов (API-режим)
|
||||
}
|
||||
|
||||
// New читает конфиг и собирает все зависимости.
|
||||
@@ -103,37 +103,26 @@ func New(configPath, version, updateToken string) (*App, error) {
|
||||
}
|
||||
log.Printf("app: db opened %s", cfg.Paths.DB)
|
||||
|
||||
// OpenCode: пул serve-процессов (по одному на каталог) + API-runner.
|
||||
// Служебный root-сервер (worktree) живёт всё время app; остальные лениво.
|
||||
ocPool := opencode.NewPool(cfg.Paths.Worktree)
|
||||
ocPool.Bin = cfg.OpenCode.Bin
|
||||
ocPool.Config = cfg.OpenCode.Config
|
||||
ocPool.ConfigDir = cfg.OpenCode.ConfigDir
|
||||
ocPool.DBPath = cfg.OpenCode.DBPath
|
||||
ocPool.Host = cfg.OpenCode.Serve.Hostname
|
||||
ocPool.BasePort = cfg.OpenCode.Serve.Port
|
||||
ocPool.Password = cfg.OpenCode.Serve.Password
|
||||
|
||||
// OpenCode runner — один на аналитика и воркер
|
||||
ocRunner := &opencode.Runner{
|
||||
Bin: cfg.OpenCode.Bin,
|
||||
DBPath: cfg.OpenCode.DBPath,
|
||||
Config: cfg.OpenCode.Config,
|
||||
ConfigDir: cfg.OpenCode.ConfigDir,
|
||||
Pool: ocPool,
|
||||
IdleTimeout: cfg.OpenCode.IdleTimeout.Duration(),
|
||||
HardTimeout: cfg.OpenCode.HardTimeout.Duration(),
|
||||
PollInterval: cfg.OpenCode.PollMs.Duration(),
|
||||
Stdout: os.Stderr,
|
||||
}
|
||||
|
||||
// Режим opencode serve (--attach): супервайзер держит постоянный сервер и
|
||||
// подключает Runner к нему. Включается serve.enabled (свой процесс) либо
|
||||
// serve.url (внешний). Выключено по умолчанию — прежняя spawn-модель.
|
||||
var serveSrv *opencode.Server
|
||||
if cfg.OpenCode.Serve.Enabled || cfg.OpenCode.Serve.URL != "" {
|
||||
serveSrv = &opencode.Server{
|
||||
Bin: cfg.OpenCode.Bin,
|
||||
DBPath: cfg.OpenCode.DBPath,
|
||||
Config: cfg.OpenCode.Config,
|
||||
ConfigDir: cfg.OpenCode.ConfigDir,
|
||||
Host: cfg.OpenCode.Serve.Hostname,
|
||||
Port: cfg.OpenCode.Serve.Port,
|
||||
Password: cfg.OpenCode.Serve.Password,
|
||||
URL: cfg.OpenCode.Serve.URL,
|
||||
Stdout: os.Stderr,
|
||||
}
|
||||
ocRunner.AttachURL = serveSrv.Addr()
|
||||
}
|
||||
|
||||
// Analyst (Decider)
|
||||
analystCtx := &analyst.Analyst{
|
||||
Runner: ocRunner,
|
||||
@@ -152,7 +141,7 @@ func New(configPath, version, updateToken string) (*App, error) {
|
||||
Config: cfg,
|
||||
Store: store,
|
||||
CoreCtx: coreCtx,
|
||||
serve: serveSrv,
|
||||
pool: ocPool,
|
||||
}
|
||||
router := chat.NewRouter(a.handleIncoming)
|
||||
|
||||
@@ -206,17 +195,19 @@ func (a *App) Run(ctx context.Context) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// opencode serve: поднимаем супервайзер до старта воркера (иначе первый
|
||||
// вызов --attach упрётся в несуществующий сервер). При неудаче — не стартуем.
|
||||
if a.serve != nil {
|
||||
if err := a.serve.Start(ctx); err != nil {
|
||||
return fmt.Errorf("opencode serve: %w", err)
|
||||
}
|
||||
log.Printf("app: opencode serve up at %s (attach mode)", a.serve.Addr())
|
||||
go a.serve.Run(ctx)
|
||||
defer a.serve.Close()
|
||||
// Уже отменённый контекст — не поднимаем подсистемы, graceful shutdown сразу.
|
||||
if ctx.Err() != nil {
|
||||
log.Print("app: context already cancelled, skipped start")
|
||||
return nil
|
||||
}
|
||||
|
||||
// opencode serve: поднимаем служебный корневой сервер (worktree) до старта
|
||||
// воркера, остальные каталоги — лениво. При неудаче — не стартуем.
|
||||
if err := a.pool.EnsureRoot(ctx); err != nil {
|
||||
return fmt.Errorf("opencode: %w", err)
|
||||
}
|
||||
defer a.pool.Close()
|
||||
|
||||
// Канал для проверки Telegram-ошибки (горутина оборачивает Run)
|
||||
tgErr := make(chan error, 1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user