Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 52 additions & 5 deletions cmd/agenttrace/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type doctorReport struct {
CacheEntries int `json:"cache_entries"`
CacheDirs int `json:"cache_dirs"`
CachedValid int `json:"cached_valid"`
Sessions int `json:"sessions"`
SessionFiles int `json:"session_files"`
Directories []doctorDirReport `json:"directories"`
Recommendations []string `json:"recommendations"`
Expand All @@ -44,7 +45,11 @@ func renderDoctorReport(dir string, demo bool, format string) (string, error) {

func buildDoctorReport(dir string, demo bool) doctorReport {
cache := engine.LoadSessionCache()
files := engine.FindSessionFilesCached(dir, cache)
files := engine.FindReportableSessionFilesCached(dir, cache)
var sqliteSessions []engine.Session
if dir == "" && !demo {
sqliteSessions = engine.LoadSQLiteBackedSessions()
}
valid := engine.ValidCachedSessionCount(files, cache)

mode := i18n.T("doctor_mode_auto")
Expand All @@ -62,14 +67,15 @@ func buildDoctorReport(dir string, demo bool) doctorReport {
CacheEntries: cache.EntryCount(),
CacheDirs: len(cache.Dirs),
CachedValid: valid,
Sessions: len(files) + len(sqliteSessions),
SessionFiles: len(files),
Directories: doctorDirectories(dir, files),
Directories: doctorDirectories(dir, files, sqliteSessions),
}
report.Recommendations = doctorRecommendations(report, dir, demo)
return report
}

func doctorDirectories(dir string, files []string) []doctorDirReport {
func doctorDirectories(dir string, files []string, sqliteSessions []engine.Session) []doctorDirReport {
var dirs []doctorDirReport
if dir != "" {
abs, err := filepath.Abs(dir)
Expand Down Expand Up @@ -103,11 +109,47 @@ func doctorDirectories(dir string, files []string) []doctorDirReport {
Files: countByRoot[candidate.Path],
})
}
dirs = append(dirs, doctorSQLiteDirectories(sqliteSessions)...)
return dirs
}

func doctorSQLiteDirectories(sessions []engine.Session) []doctorDirReport {
home, _ := os.UserHomeDir()
if home == "" {
return nil
}
countByTool := make(map[string]int)
for _, session := range sessions {
countByTool[session.Metrics.SourceTool]++
}

candidates := []struct {
tool string
name string
path string
}{
{tool: "hermes_db", name: "Hermes Agent (DB)", path: filepath.Join(home, ".hermes", "state.db")},
{tool: "opencode_db", name: "OpenCode (DB)", path: filepath.Join(home, ".local", "share", "opencode", "opencode.db")},
}

var dirs []doctorDirReport
for _, candidate := range candidates {
exists := fileExists(candidate.path)
if !exists && countByTool[candidate.tool] == 0 {
continue
}
dirs = append(dirs, doctorDirReport{
Name: candidate.name,
Path: candidate.path,
Exists: exists,
Files: countByTool[candidate.tool],
})
}
return dirs
}

func doctorRecommendations(report doctorReport, dir string, demo bool) []string {
if report.SessionFiles == 0 {
if report.Sessions == 0 {
if dir != "" {
return []string{i18n.T("doctor_next_custom")}
}
Expand All @@ -129,7 +171,7 @@ func doctorReportText(report doctorReport) string {
fmt.Fprintf(&b, "%s\n", i18n.T("doctor_title"))
fmt.Fprintf(&b, "%s: %s\n", i18n.T("doctor_version"), report.Version)
fmt.Fprintf(&b, "%s: %s\n", i18n.T("doctor_mode"), report.Mode)
fmt.Fprintf(&b, "%s: %d\n", i18n.T("doctor_session_files"), report.SessionFiles)
fmt.Fprintf(&b, "%s: %d\n", i18n.T("doctor_session_files"), report.Sessions)
fmt.Fprintf(&b, "%s: %s\n", i18n.T("doctor_cache"), report.CachePath)
fmt.Fprintf(&b, " "+i18n.T("doctor_cache_detail")+"\n", report.CacheEntries, report.CachedValid, report.CacheDirs)
fmt.Fprintf(&b, "\n%s:\n", i18n.T("doctor_directories"))
Expand All @@ -151,3 +193,8 @@ func dirExists(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir()
}

func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
81 changes: 79 additions & 2 deletions cmd/agenttrace/doctor_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package main

import (
"database/sql"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"

"github.com/luoyuctl/agenttrace/internal/engine"
"github.com/luoyuctl/agenttrace/internal/i18n"

_ "modernc.org/sqlite"
)

func TestDoctorReportWithDemoSessions(t *testing.T) {
Expand All @@ -24,7 +29,7 @@ func TestDoctorReportWithDemoSessions(t *testing.T) {
if err := json.Unmarshal([]byte(out), &report); err != nil {
t.Fatal(err)
}
if report.Version != engine.Version || report.SessionFiles != 3 || len(report.Directories) != 1 {
if report.Version != engine.Version || report.Sessions != 3 || report.SessionFiles != 3 || len(report.Directories) != 1 {
t.Fatalf("unexpected doctor report: %+v", report)
}
if !report.Directories[0].Exists || report.Directories[0].Files != 3 {
Expand All @@ -35,6 +40,45 @@ func TestDoctorReportWithDemoSessions(t *testing.T) {
}
}

func TestDoctorReportUsesReportableSQLiteBackedSources(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))

sessionsDir := filepath.Join(home, ".hermes", "sessions")
if err := os.MkdirAll(sessionsDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sessionsDir, "legacy.jsonl"), []byte("{}\n"), 0644); err != nil {
t.Fatal(err)
}
if err := writeDoctorHermesStateDBForTest(filepath.Join(home, ".hermes", "state.db")); err != nil {
t.Fatal(err)
}

report := buildDoctorReport("", false)
if report.Sessions != 1 || report.SessionFiles != 0 {
t.Fatalf("doctor should split sessions from reportable files, got sessions=%d files=%d", report.Sessions, report.SessionFiles)
}

var hermesFileRow, hermesDBRow *doctorDirReport
for i := range report.Directories {
switch report.Directories[i].Name {
case "Hermes Agent":
hermesFileRow = &report.Directories[i]
case "Hermes Agent (DB)":
hermesDBRow = &report.Directories[i]
}
}
if hermesFileRow == nil || hermesFileRow.Files != 0 {
t.Fatalf("sqlite-backed legacy file dir should be skipped, got %+v", hermesFileRow)
}
if hermesDBRow == nil || !hermesDBRow.Exists || hermesDBRow.Files != 1 {
t.Fatalf("sqlite-backed source should be shown, got %+v", hermesDBRow)
}
}

func TestDoctorReportChineseText(t *testing.T) {
prev := i18n.Current
i18n.SetLang(i18n.ZH)
Expand All @@ -44,9 +88,42 @@ func TestDoctorReportChineseText(t *testing.T) {
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"AGENTTRACE 诊断", "会话文件", "建议"} {
for _, want := range []string{"AGENTTRACE 诊断", "会话", "建议"} {
if !strings.Contains(out, want) {
t.Fatalf("doctor output missing %q:\n%s", want, out)
}
}
}

func writeDoctorHermesStateDBForTest(path string) error {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
db, err := sql.Open("sqlite", path)
if err != nil {
return err
}
defer db.Close()
if _, err := db.Exec(`create table sessions (
id text primary key,
model text,
started_at real,
ended_at real,
message_count integer,
tool_call_count integer,
input_tokens integer,
output_tokens integer,
cache_read_tokens integer,
cache_write_tokens integer
)`); err != nil {
return err
}
if _, err := db.Exec(`create table messages (session_id text, role text)`); err != nil {
return err
}
if _, err := db.Exec(`insert into sessions values ('db-session', 'gpt-5.1', 1760000000, 1760000060, 2, 1, 1000, 200, 50, 25)`); err != nil {
return err
}
_, err = db.Exec(`insert into messages values ('db-session', 'user'), ('db-session', 'assistant')`)
return err
}
4 changes: 2 additions & 2 deletions internal/i18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -1305,10 +1305,10 @@ var translations = map[string]map[Lang]string{
"doctor_mode_custom": {EN: "custom directory", ZH: "指定目录"},
"doctor_mode_demo": {EN: "demo sessions", ZH: "演示会话"},
"doctor_version": {EN: "Version", ZH: "版本"},
"doctor_session_files": {EN: "Session files", ZH: "会话文件"},
"doctor_session_files": {EN: "Sessions", ZH: "会话"},
"doctor_cache": {EN: "Cache", ZH: "缓存"},
"doctor_cache_detail": {EN: "%d entries, %d valid for current scan, %d cached directories", ZH: "%d 条记录,当前扫描命中 %d 条,%d 个目录缓存"},
"doctor_directories": {EN: "Directories", ZH: "目录"},
"doctor_directories": {EN: "Sources", ZH: "来源"},
"doctor_found": {EN: "found", ZH: "存在"},
"doctor_missing": {EN: "missing", ZH: "缺失"},
"doctor_recommendations": {EN: "Recommendations", ZH: "建议"},
Expand Down