From 8c91c83024f8aae0e03fb4881a1804aeed335483 Mon Sep 17 00:00:00 2001 From: "ki.sagidullin" Date: Sun, 23 Aug 2026 17:54:35 +0500 Subject: [PATCH] =?UTF-8?q?fix(opencode):=20=D0=B0=D0=B3=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D1=8B=20=E2=80=94=20=D0=B2=20/.opencode/agent,=20OPE?= =?UTF-8?q?NCODE=5FCONFIG=5FDIR=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B2=D1=8B=D1=81=D1=82=D0=B0=D0=B2=D0=BB=D1=8F?= =?UTF-8?q?=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OPENCODE_CONFIG_DIR в opencode v1.18.18 перенаправляет Global.Path.config (global.ts: config = OPENCODE_CONFIG_DIR ?? ~/.config/opencode), из-за чего глобальный конфиг (модель/провайдеры, напр. tokentool) не загружался и opencode уходил в fallback-модель. Агенты открывались, т.к. OPENCODE_CONFIG_DIR дополнительно сканируется как каталог для agent/*.md. Теперь агенты распаковываются в /.opencode/agent/*.md, где opencode находит их через project-каталог .opencode (paths.ts, cwd=worktree). Глобальный конфиг не трогаем вовсе. - internal/opencode: удалены Server.ConfigDir/Pool.ConfigDir и env OPENCODE_CONFIG_DIR - internal/config: удалено поле OpenCodeCfg.ConfigDir (config_dir) - internal/app: ensureAgentsDir пишет в /.opencode/agent - internal/agents: WriteTo(dir) → dir/agent/*.md - README/config.yaml.example/memory обновлены --- .serena/memories/core.md | 2 +- README.md | 7 ++++--- config.yaml.example | 6 ++++-- internal/agents/agents.go | 14 ++++++-------- internal/agents/agents_test.go | 4 ++-- internal/app/app.go | 25 +++++++++++-------------- internal/config/config_test.go | 28 ---------------------------- internal/config/types.go | 1 - internal/opencode/pool.go | 27 ++++++++++++--------------- internal/opencode/server.go | 8 ++++---- 10 files changed, 44 insertions(+), 78 deletions(-) diff --git a/.serena/memories/core.md b/.serena/memories/core.md index 4883252..b9f1f91 100644 --- a/.serena/memories/core.md +++ b/.serena/memories/core.md @@ -46,7 +46,7 @@ docs/ ui-spec.md — спека Fyne UI (слои, event-bus, fyne.Do - Интеграция с субагентами — через headless `opencode serve`, **v2 API** (`/api/*`). Версия opencode >= 1.18.18. - **Модель — только глобальный конфиг opencode.** Ratatoskr модель не выбирает и про неё не знает: opencode сам берёт модель по умолчанию из своего глобального конфига. Код opencode-конфиг не читает (config.go удалён). -- **Свой агент:** при создании сессии в `POST /api/session` передаётся `agent` (analyst/dev/reviewer/chat/postmortem) из встроенных определений (`internal/agents`, распаковка в `OPENCODE_CONFIG_DIR`). +- **Свой агент:** при создании сессии в `POST /api/session` передаётся `agent` (analyst/dev/reviewer/chat/postmortem) из встроенных определений. Агенты распаковываются в `/.opencode/agent/*.md` (opencode находит через project-каталог `.opencode`); `OPENCODE_CONFIG_DIR` НЕ выставлять — он перенаправляет Global.Path.config и ломает глобальный конфиг (модель/провайдеры) → падение в fallback. - **Поллинг вердикта:** `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. ## Контракты (не ломать) diff --git a/README.md b/README.md index fb509c8..4b79bca 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,6 @@ telegram: chat_id: "..." # TG_CHAT_ID (обязательно, если enabled) opencode: bin: "opencode" - config_dir: "./agents" # каталог, куда распаковываются встроенные агенты paths: db: "./ratatoskr.db" # дефолт; резолвится от каталога бинаря (не от cwd) worktree: "./worktrees" # то же правило «всё рядом с .exe» @@ -144,8 +143,10 @@ update: глобального конфига (`~/.config/opencode/opencode.jsonc`). Наш код конфиг opencode не читает. - **Свой агент.** При создании сессии в `POST /api/session` передаётся имя - встроенного агента ratatoskr (analyst/dev/reviewer/chat/postmortem), который - распаковывается в каталог `OPENCODE_CONFIG_DIR` (см. `internal/agents`). + встроенного агента ratatoskr (analyst/dev/reviewer/chat/postmortem). Агенты + распаковываются в `/.opencode/agent/*.md`, где opencode находит + их через project-каталог `.opencode` (см. `internal/agents`); глобальный + конфиг при этом не трогается. - **Поллинг вердикта.** Промпт отправляется неблокирующе (`POST .../prompt` → durable admit), вердикт собирается из новых assistant-сообщений (`GET .../message`); завершение ответа — сессия ушла из активных дренажей diff --git a/config.yaml.example b/config.yaml.example index e6174a9..e842d4b 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -10,10 +10,12 @@ telegram: # Всё ниже — опционально, показаны дефолты: # opencode: # bin: "opencode" -# config: "/путь/к/opencode.json" # файл-конфиг модели (OPENCODE_CONFIG) -# config_dir: "./agents" # каталог с агентами (OPENCODE_CONFIG_DIR) +# config: "/путь/к/opencode.json" # файл-конфиг модели (OPENCODE_CONFIG); пусто = глобальный конфиг opencode # hard_timeout: "20m" # idle_timeout: "5m" +# +# Агенты (analyst/dev/reviewer/...) распаковываются автоматически в +# /.opencode/agent/*.md; конфиг opencode для этого не нужен. # git (источник репозиториев для git clone) # base_url: "http://gitea.hal9000.home" # базовый URL git-хоста (обязательно для клонирования) diff --git a/internal/agents/agents.go b/internal/agents/agents.go index e356a41..5dbb809 100644 --- a/internal/agents/agents.go +++ b/internal/agents/agents.go @@ -1,12 +1,10 @@ // Package agents — встроенные агенты opencode (analyst, dev, ...). // // Агенты зашиты в бинарь через go:embed и распаковываются при запуске -// в каталог OPENCODE_CONFIG_DIR (по умолчанию ./agents рядом с бинарём), -// чтобы opencode находил их независимо от того, где запущен ratatoskr. -// -// opencode сканирует OPENCODE_CONFIG_DIR как каталог .opencode: агенты -// ищутся в поддиректории agents/ (мн.ч.), а не в корне. Поэтому распаковка -// идёт в /agents/*.md. +// в project-каталог /.opencode/agent/*.md. opencode при serve +// (cwd = worktree) обнаруживает .opencode поднятием вверх (ConfigPaths) и +// находит агентов по шаблону {agent,agents}/**/*.md. Глобальный конфиг +// opencode остаётся нетронутым (OPENCODE_CONFIG_DIR не выставляем). package agents import ( @@ -23,10 +21,10 @@ var FS embed.FS // в FS, но для детерминированной распаковки список лучше дополнять здесь. var Names = []string{"analyst", "dev", "reviewer", "chat", "postmortem"} -// WriteTo распаковывает всех встроенных агентов в каталог dir/agents +// WriteTo распаковывает всех встроенных агентов в каталог dir/agent // (создаёт его). Файлы перезаписываются — встроенная копия всегда актуальна. func WriteTo(dir string) error { - agentsDir := filepath.Join(dir, "agents") + agentsDir := filepath.Join(dir, "agent") for _, name := range Names { data, err := FS.ReadFile(name + ".md") if err != nil { diff --git a/internal/agents/agents_test.go b/internal/agents/agents_test.go index f91da13..8deb103 100644 --- a/internal/agents/agents_test.go +++ b/internal/agents/agents_test.go @@ -13,8 +13,8 @@ func TestWriteTo(t *testing.T) { t.Fatalf("WriteTo: %v", err) } for _, name := range Names { - // opencode ищет агентов в поддиректории agents/, как у .opencode - p := filepath.Join(dir, "agents", name+".md") + // opencode ищет агентов в поддиректории agent/ (или agents/) внутри .opencode + p := filepath.Join(dir, "agent", name+".md") data, err := os.ReadFile(p) if err != nil { t.Fatalf("read %s: %v", p, err) diff --git a/internal/app/app.go b/internal/app/app.go index 3d0f200..bda1c1b 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -157,7 +157,6 @@ func New(configPath, version, updateToken string, noUI bool) (*App, error) { 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 @@ -665,23 +664,21 @@ func (a *App) handleUpdateCmd(ctx context.Context, uid chat.UserID) { // сюда не возвращаемся — SwapAndRestart завершил процесс (os.Exit) } -// ensureAgentsDir определяет каталог с агентами opencode и распаковывает -// туда встроенных агентов (go:embed). Если config_dir не задан — использует -// ./agents рядом с бинарём. Встроенные агенты перезаписываются (всегда актуальны). +// ensureAgentsDir распаковывает встроенных агентов opencode (go:embed) в +// project-каталог /.opencode/agent/*.md. opencode при serve (cwd = +// worktree) обнаруживает .opencode поднятием вверх (paths.ts) и сам находит +// агентов. Глобальный конфиг opencode (модель/провайдеры) не трогаем и +// OPENCODE_CONFIG_DIR не выставляем. // // idempotent: вызывается только из New. func ensureAgentsDir(cfg *config.Config) error { - if cfg.OpenCode.ConfigDir == "" { - exe, err := os.Executable() - if err != nil { - return fmt.Errorf("resolve executable: %w", err) - } - // ./agents рядом с бинарём - cfg.OpenCode.ConfigDir = filepath.Join(filepath.Dir(exe), "agents") + if cfg.Paths.Worktree == "" { + return fmt.Errorf("paths.worktree не задан — некуда раскладывать агентов") } - if err := agents.WriteTo(cfg.OpenCode.ConfigDir); err != nil { - return fmt.Errorf("write agents to %s: %w", cfg.OpenCode.ConfigDir, err) + base := filepath.Join(cfg.Paths.Worktree, ".opencode") + if err := agents.WriteTo(base); err != nil { + return fmt.Errorf("write agents to %s: %w", base, err) } - log.Printf("app: agents ensured in %s", cfg.OpenCode.ConfigDir) + log.Printf("app: agents ensured in %s", base) return nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index de237f8..41393ca 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -146,34 +146,6 @@ log: } } -func TestLoad_OpenCodeConfigDir(t *testing.T) { - t.Setenv("TG_TOKEN", "tok") - t.Setenv("TG_CHAT_ID", "42") - - // без config_dir — пусто - yaml := `telegram: - token: "${TG_TOKEN}" - chat_id: "${TG_CHAT_ID}" -` - cfg, err := Load(writeCfg(t, yaml)) - if err != nil { - t.Fatalf("Load: %v", err) - } - if cfg.OpenCode.ConfigDir != "" { - t.Errorf("config_dir = %q, want пусто", cfg.OpenCode.ConfigDir) - } - - // через env OPENCODE_CONFIG_DIR - t.Setenv("OPENCODE_CONFIG_DIR", "/opt/agents") - cfg, err = Load(writeCfg(t, yaml)) - if err != nil { - t.Fatalf("Load: %v", err) - } - if cfg.OpenCode.ConfigDir != "/opt/agents" { - t.Errorf("config_dir = %q, want /opt/agents", cfg.OpenCode.ConfigDir) - } -} - func TestLoad_EnvOverride(t *testing.T) { t.Setenv("TG_TOKEN", "tok") t.Setenv("TG_CHAT_ID", "42") diff --git a/internal/config/types.go b/internal/config/types.go index fd22c2d..03fe381 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -89,7 +89,6 @@ type OpenCodeCfg struct { Bin string `yaml:"bin" default:"opencode"` DBPath string `yaml:"db_path" default:""` Config string `yaml:"config" env:"OPENCODE_CONFIG"` - ConfigDir string `yaml:"config_dir" env:"OPENCODE_CONFIG_DIR"` HardTimeout Duration `yaml:"hard_timeout" default:"20m"` IdleTimeout Duration `yaml:"idle_timeout" default:"5m"` PollMs Duration `yaml:"poll_ms" default:"2s"` diff --git a/internal/opencode/pool.go b/internal/opencode/pool.go index 06ebfa1..c1ad830 100644 --- a/internal/opencode/pool.go +++ b/internal/opencode/pool.go @@ -20,7 +20,6 @@ import ( type Pool struct { Bin string Config string - ConfigDir string DBPath string Host string BasePort int @@ -72,13 +71,12 @@ func (p *Pool) EnsureRoot(ctx context.Context) error { return nil } s := &Server{ - Bin: p.Bin, - Config: p.Config, - ConfigDir: p.ConfigDir, - DBPath: p.DBPath, - Host: p.Host, - Password: p.Password, - Dir: p.rootDir, + Bin: p.Bin, + Config: p.Config, + DBPath: p.DBPath, + Host: p.Host, + Password: p.Password, + Dir: p.rootDir, } if err := p.assign(s); err != nil { return err @@ -102,13 +100,12 @@ func (p *Pool) Ensure(ctx context.Context, dir string) (*Server, error) { } abs := filepath.Clean(dir) s := &Server{ - Bin: p.Bin, - Config: p.Config, - ConfigDir: p.ConfigDir, - DBPath: p.DBPath, - Host: p.Host, - Password: p.Password, - Dir: abs, + Bin: p.Bin, + Config: p.Config, + DBPath: p.DBPath, + Host: p.Host, + Password: p.Password, + Dir: abs, } if err := p.assign(s); err != nil { p.mu.Unlock() diff --git a/internal/opencode/server.go b/internal/opencode/server.go index c8aecb8..ce905e9 100644 --- a/internal/opencode/server.go +++ b/internal/opencode/server.go @@ -25,7 +25,6 @@ import ( type Server struct { Bin string // путь к opencode (по умолчанию "opencode") Config string // OPENCODE_CONFIG - ConfigDir string // OPENCODE_CONFIG_DIR DBPath string // рабочая БД сервера (передам env, если задана) Host string // hostname для прослушивания @@ -127,12 +126,13 @@ func (s *Server) serveCmd(ctx context.Context) *exec.Cmd { env := append(os.Environ(), "OPENCODE_DISABLE_AUTOUPDATE=1", "OPENCODE_DISABLE_MODELS_FETCH=1") + // Агенты (analyst/dev/...) opencode находит сам через project-каталог + // .opencode (см. internal/agents); OPENCODE_CONFIG_DIR не выставляем, + // чтобы не перенаправлять Global.Path.config и сохранить глобальный + // конфиг opencode (модель/провайдеры). if s.Config != "" { env = append(env, "OPENCODE_CONFIG="+s.Config) } - if s.ConfigDir != "" { - env = append(env, "OPENCODE_CONFIG_DIR="+s.ConfigDir) - } if s.DBPath != "" { env = append(env, "OPENCODE_DB="+s.DBPath) }