feat: opencode serve — постоянный сервер вместо разовых subprocess (вариант A)
Some checks failed
CI / test (push) Successful in 54s
CI / build-and-package (amd64, linux) (push) Failing after 1h58m55s
CI / build-and-package (amd64, windows) (push) Successful in 59s

Супервайзер 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}.
This commit is contained in:
Hermes
2026-08-18 08:35:09 +05:00
parent ed11879cbd
commit 2f26b6ae88
8 changed files with 618 additions and 1 deletions

View File

@@ -60,6 +60,7 @@ type App struct {
Worker *worker.Worker
Updater *update.Updater
tg *telegram.Channel // сохранена для Run
serve *opencode.Server // супервайзер opencode serve (режим --attach); nil, если выключен
}
// New читает конфиг и собирает все зависимости.
@@ -114,6 +115,25 @@ func New(configPath, version, updateToken string) (*App, error) {
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,
@@ -132,6 +152,7 @@ func New(configPath, version, updateToken string) (*App, error) {
Config: cfg,
Store: store,
CoreCtx: coreCtx,
serve: serveSrv,
}
router := chat.NewRouter(a.handleIncoming)
@@ -185,6 +206,17 @@ 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()
}
// Канал для проверки Telegram-ошибки (горутина оборачивает Run)
tgErr := make(chan error, 1)