diff --git a/internal/agents/agents.go b/internal/agents/agents.go index 5dbb809..a150092 100644 --- a/internal/agents/agents.go +++ b/internal/agents/agents.go @@ -24,7 +24,7 @@ var Names = []string{"analyst", "dev", "reviewer", "chat", "postmortem"} // WriteTo распаковывает всех встроенных агентов в каталог dir/agent // (создаёт его). Файлы перезаписываются — встроенная копия всегда актуальна. func WriteTo(dir string) error { - agentsDir := filepath.Join(dir, "agent") + agentsDir := filepath.Join(dir, "agents") for _, name := range Names { data, err := FS.ReadFile(name + ".md") if err != nil { diff --git a/internal/opencode/client.go b/internal/opencode/client.go index 703c68b..9b86cba 100644 --- a/internal/opencode/client.go +++ b/internal/opencode/client.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "time" ) @@ -24,10 +23,11 @@ import ( // Prompt не блокирует: вердикт собирается поллингом из content[].type=="text" // новых assistant-сообщений (см. Runner.awaitVerdict). type Client struct { - BaseURL string // http://host:port (без завершающего слеша) - Password string // basic auth (username "opencode") - Debug bool // включать отладочные логи API-вызовов (log.level=debug) - http *http.Client // единый клиент: все операции быстрые (нет блокирующего Send) + BaseURL string // http://host:port (без завершающего слеша) + Password string // basic auth (username "opencode") + Directory string // каталог сервера — идёт заголовком x-opencode-directory + Debug bool // включать отладочные логи API-вызовов (log.level=debug) + http *http.Client // единый клиент: все операции быстрые (нет блокирующего Send) } // ClientErr — классы ошибок клиента. @@ -56,14 +56,15 @@ func (c *Client) do(ctx context.Context, method, path, op string, body []byte) ( if err != nil { return nil, &ClientErr{Op: "connect", Err: err} } + if body != nil { + req.ContentLength = int64(len(body)) + } if c.Password != "" { req.SetBasicAuth("opencode", c.Password) } - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - if c.Debug { - log.Printf("opencode api debug: %s -> %s %s%s", op, method, c.BaseURL, path) + req.Header.Set("Content-Type", "application/json") + if c.Directory != "" { + req.Header.Set("x-opencode-directory", c.Directory) } resp, err := c.http.Do(req) if err != nil { @@ -75,14 +76,8 @@ func (c *Client) do(ctx context.Context, method, path, op string, body []byte) ( return nil, &ClientErr{Op: "connect", Err: err} } if resp.StatusCode < 200 || resp.StatusCode > 299 { - if c.Debug { - log.Printf("opencode api debug: %s response: status %d", op, resp.StatusCode) - } return nil, &ClientErr{Op: op, Err: fmt.Errorf("status %d: %s", resp.StatusCode, truncateStr(string(b), 300))} } - if c.Debug { - log.Printf("opencode api debug: %s response (%d bytes)", op, len(b)) - } return b, nil } @@ -130,6 +125,12 @@ func (c *Client) CreateSession(ctx context.Context, agent string) (string, error if out.Data.ID == "" { return "", &ClientErr{Op: "create", Err: fmt.Errorf("пустой id сессии")} } + time.Sleep(2 * time.Second) + // Холостой вызов /api/model: прогревает сервер (модель/провайдеры), + // чтобы первый промпт не спотыкался о «тёплый» старт. + _, _ = c.do(ctx, http.MethodGet, "/api/model", "models-warmup", nil) + time.Sleep(2 * time.Second) + _, _ = c.do(ctx, http.MethodGet, "/api/model", "models-warmup", nil) return out.Data.ID, nil } diff --git a/internal/opencode/runner.go b/internal/opencode/runner.go index 6c175e3..35658a9 100644 --- a/internal/opencode/runner.go +++ b/internal/opencode/runner.go @@ -73,7 +73,7 @@ func (r *Runner) Run(ctx context.Context, prompt, cwd, agent, sessionID string) if err != nil { return nil, err } - c := &Client{BaseURL: srv.Addr(), Password: srv.Password, Debug: r.Debug} + c := &Client{BaseURL: srv.Addr(), Password: srv.Password, Directory: srv.Dir, Debug: r.Debug} // Сессия: заданная (resume) или новая. Модель НЕ выбираем — opencode сам // берёт её по умолчанию из своего глобального конфига. diff --git a/internal/opencode/runner_test.go b/internal/opencode/runner_test.go index f70dba4..7734cf8 100644 --- a/internal/opencode/runner_test.go +++ b/internal/opencode/runner_test.go @@ -146,7 +146,7 @@ func TestRun_ContextCancel(t *testing.T) { done <- res errCh <- err }() - time.Sleep(30 * time.Millisecond) + time.Sleep(6000 * time.Millisecond) cancel() res := <-done if err := <-errCh; err != nil { diff --git a/internal/opencode/server.go b/internal/opencode/server.go index 972850f..c58cf24 100644 --- a/internal/opencode/server.go +++ b/internal/opencode/server.go @@ -120,6 +120,8 @@ func (s *Server) Start(ctx context.Context) error { s.done = done s.mu.Unlock() + log.Printf("opencode serve: spawn dir=%s HOME=%q USERPROFILE=%q OPENCODE_CONFIG=%q (serve наследует окружение родителя)", cmd.Dir, os.Getenv("HOME"), os.Getenv("USERPROFILE"), s.ConfigPath) + if err := cmd.Start(); err != nil { return fmt.Errorf("opencode serve: start %v: %w", s.Bin, err) } @@ -143,24 +145,8 @@ func (s *Server) serveCmd(ctx context.Context) *exec.Cmd { // Своя process-group: чтобы killGroup (по pgid) убивал только сервер и его // дочерние процессы, а не чужой процесс (например, тест-реннер). setpgid(cmd) - env := append(os.Environ(), - "OPENCODE_DISABLE_AUTOUPDATE=1", - "OPENCODE_DISABLE_MODELS_FETCH=1") - // Гарантированно указываем глобальный конфиг opencode: serve обязан - // подхватить модель/провайдеров из него (иначе возможен фоллбэк на - // случайную модель из каталога). - if s.ConfigPath != "" { - env = append(env, "OPENCODE_CONFIG="+s.ConfigPath) - } - // Агенты (analyst/dev/...) opencode находит сам через project-каталог - // .opencode (см. internal/agents). - if s.DBPath != "" { - env = append(env, "OPENCODE_DB="+s.DBPath) - } - if s.Password != "" { - env = append(env, "OPENCODE_SERVER_PASSWORD="+s.Password) - } - cmd.Env = env + // Env не задаём: serve наследует окружение родителя и сам резолвит свой + // глобальный/локальный конфиг (модель/провайдеры, агентов, БД) как обычно. return cmd } diff --git a/internal/opencode/server_test.go b/internal/opencode/server_test.go index 0606469..a63014b 100644 --- a/internal/opencode/server_test.go +++ b/internal/opencode/server_test.go @@ -200,29 +200,19 @@ func atoiOrZero(s string) int { return n } -func TestServeCmd_SetsOpenCodeConfig(t *testing.T) { - // serveCmd должен всегда передавать OPENCODE_CONFIG: без него serve может - // не подхватить модель/провайдеров из глобального конфига. +func TestServeCmd_NoCustomEnv(t *testing.T) { + // serveCmd не должен задавать свой Env: serve наследует окружение родителя + // и сам резолвит конфиг/БД/пароль как обычно. s := &Server{} s.defaults() cmd := s.serveCmd(context.Background()) - if cmd.Env == nil { - t.Fatal("serveCmd: Env не задан") - } - prefix := "OPENCODE_CONFIG=" - path := "" - for _, kv := range cmd.Env { - if strings.HasPrefix(kv, prefix) { - path = strings.TrimPrefix(kv, prefix) - break + if cmd.Env != nil { + for _, kv := range cmd.Env { + if strings.HasPrefix(kv, "OPENCODE_") { + t.Errorf("serveCmd: не ожидал env %q", kv) + } } } - if path == "" { - t.Fatal("serveCmd: OPENCODE_CONFIG не выставлен") - } - if want := s.ConfigPath; path != want { - t.Errorf("OPENCODE_CONFIG = %q, want %q", path, want) - } } func TestServerCmd_CustomOpenCodeConfig(t *testing.T) {