refactor: чистка мёртвого кода, лимит ходов D3, HTML-экранирование и UTF-8 обрезка в Telegram
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
// Контракты перенесены 1-в-1 из Python-версии (extract.py / opencode.py):
|
||||
// - ExtractVerdict: последний text-парт из NDJSON-потока opencode run --format json
|
||||
// - ExtractJSON: fenced ```json``` → первый {...}
|
||||
// - Run/ResumeDev: запуск процесса с idle/hard timeout по opencode.db
|
||||
package opencode
|
||||
|
||||
import (
|
||||
@@ -14,9 +13,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
fenceRe = regexp.MustCompile("```(?:json)?\\s*([\\s\\S]*?)```")
|
||||
jsonBlockRe = regexp.MustCompile("\\{[\\s\\S]*\\}")
|
||||
sessionRe = regexp.MustCompile(`"session_id"\s*:\s*"([^"]+)"`)
|
||||
fenceRe = regexp.MustCompile("```(?:json)?\\s*([\\s\\S]*?)```")
|
||||
jsonBlockRe = regexp.MustCompile("\\{[\\s\\S]*\\}")
|
||||
)
|
||||
|
||||
// ExtractVerdict возвращает текст вердикта из NDJSON-потока opencode run --format json.
|
||||
@@ -76,15 +74,6 @@ func ExtractJSON(text string) (map[string]json.RawMessage, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// SessionIDFromOutput извлекает session_id из текстового вывода opencode.
|
||||
func SessionIDFromOutput(out string) (string, bool) {
|
||||
m := sessionRe.FindStringSubmatch(out)
|
||||
if len(m) > 1 && m[1] != "" {
|
||||
return m[1], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// stripFence обрезает внешние ```json``` (или ```) ограждения вокруг фрагмента.
|
||||
// Используется для вердиктов, которые модель может вернуть в markdown-фенсе.
|
||||
func stripFence(s string) string {
|
||||
|
||||
@@ -104,12 +104,3 @@ func TestExtractJSON(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionIDFromOutput(t *testing.T) {
|
||||
if s, ok := SessionIDFromOutput(`{"session_id":"abc123"}`); !ok || s != "abc123" {
|
||||
t.Fatalf("got %q %v", s, ok)
|
||||
}
|
||||
if _, ok := SessionIDFromOutput("no session here"); ok {
|
||||
t.Fatal("expected no match")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,36 +2,11 @@ package opencode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// parseLiveStep пытается распарсить одну NDJSON-строку stdout opencode как
|
||||
// событие (text/tool/agent). Возвращает nil, если строка не является событием.
|
||||
func parseLiveStep(line string) *LiveStep {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "{") {
|
||||
return nil
|
||||
}
|
||||
var obj struct {
|
||||
Type string `json:"type"`
|
||||
Part struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"part"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(line), &obj); err != nil {
|
||||
return nil
|
||||
}
|
||||
if obj.Type == "" {
|
||||
return nil
|
||||
}
|
||||
return &LiveStep{Type: obj.Type, Text: obj.Part.Text, At: time.Now()}
|
||||
}
|
||||
|
||||
// LiveStep — один наблюдаемый шаг агента из NDJSON-потока opencode run.
|
||||
// Собирается из live-строк stdout, не из БД.
|
||||
// LiveStep — один наблюдаемый шаг агента.
|
||||
type LiveStep struct {
|
||||
Type string // "text" | "tool" | "agent" | ...
|
||||
Text string // содержимое text-парта (для других типов может быть пустым)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
//go:build linux
|
||||
|
||||
package opencode
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func sysProcAttr(proc *exec.Cmd) {
|
||||
proc.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
}
|
||||
|
||||
// killProcGroup убивает всю process-group по лидеру pid (SIGKILL дочерним и
|
||||
// SIGTERM лидеру). Игнорирует ошибки: weakest-effort teardown.
|
||||
func killProcGroup(pid int) {
|
||||
pgid, err := syscall.Getpgid(pid)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = syscall.Kill(-pgid, syscall.SIGKILL)
|
||||
_ = syscall.Kill(pid, syscall.SIGKILL)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build !linux
|
||||
|
||||
package opencode
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func sysProcAttr(_ *exec.Cmd) {}
|
||||
|
||||
func killProcGroup(pid int) {}
|
||||
@@ -164,22 +164,4 @@ func (r *Runner) awaitVerdict(ctx context.Context, c *Client, sid, agent, prompt
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ResumeDev — запуск dev-агента с resume-fallback. Если resume (sessionID)
|
||||
// падает с rc!=0 — повторяем ОДИН раз свежей сессией в том же каталоге.
|
||||
// rc=-1 (обрыв по таймауту) НЕ триггерит fallback.
|
||||
// Возвращает (result, timedOut).
|
||||
func (r *Runner) ResumeDev(ctx context.Context, prompt, cwd, sessionID string) (*Result, bool) {
|
||||
res, err := r.Run(ctx, prompt, cwd, "dev", sessionID)
|
||||
if err != nil {
|
||||
return res, false
|
||||
}
|
||||
if res.RC != 0 && res.RC != -1 && sessionID != "" {
|
||||
r.logf("dev resume rc=%d — запускаю заново свежей сессией (каталог сохраняю)", res.RC)
|
||||
res, _ = r.Run(ctx, prompt+resumeFallbackNote, cwd, "dev", "")
|
||||
}
|
||||
return res, res.RC == -1
|
||||
}
|
||||
|
||||
const resumeFallbackNote = "\n\n(Возобновление сессии не удалось; продолжи с учётом уже сделанных изменений в worktree.)"
|
||||
}
|
||||
@@ -93,26 +93,6 @@ func TestRun_ContextCancel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestResumeDev_Fallback: resume (sessionID) "падает" rc!=0 только когда самого
|
||||
// сервера нет; в фейке такого нет, поэтому проверяем, что при успехе
|
||||
// fallback не срабатывает и таймаут не выставляется.
|
||||
func TestResumeDev_NoFallbackOnSuccess(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
f := &fakeAPIServer{
|
||||
verdictParts: []part{{Type: "text", Text: "ok"}},
|
||||
}
|
||||
p, _ := fakePool(t, f, dir)
|
||||
|
||||
r := &Runner{Pool: p, PollInterval: 5 * time.Millisecond}
|
||||
res, timedOut := r.ResumeDev(context.Background(), "task", dir, "lost-session")
|
||||
if timedOut {
|
||||
t.Error("timedOut = true, want false (успех не должен считаться таймаутом)")
|
||||
}
|
||||
if res.RC != 0 {
|
||||
t.Errorf("RC = %d, want 0", res.RC)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, sub string) bool {
|
||||
return len(s) >= len(sub) && (s == sub || len(s) > 0 && indexOf(s, sub) >= 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user