Skip to content

Commit 9317305

Browse files
fix(active): resolve symlinks in session detection, fix session_stats project name
Three fixes: 1. FindActiveSessionPath now resolves symlinks on claudeHome before scanning. ~/.claude → ~/workspace/.claude caused a path mismatch: os.ReadDir built paths through the symlink while lsof reported resolved paths, so the pathSet lookup always failed. 2. lsof scoped to -c claude to avoid matching macOS Spotlight/mds processes that hold stale JSONL files open for indexing. 3. get_session_stats used filepath.Base(meta.ProjectPath) for live sessions — same bug fixed earlier in health_tools. Now uses resolveProjectName for consistency.
1 parent aa7b893 commit 9317305

3 files changed

Lines changed: 17 additions & 9 deletions

File tree

internal/claude/active.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ type assistantMsgUsage struct {
3333
// Returns ("", nil) if no active session is found.
3434
// Returns ("", error) only on unexpected I/O failure.
3535
func FindActiveSessionPath(claudeHome string) (string, error) {
36+
// Resolve symlinks so that paths match lsof output. On macOS it's common
37+
// for ~/.claude to be a symlink (e.g. → ~/workspace/.claude); lsof reports
38+
// the resolved path, so our pathSet must use resolved paths too.
39+
resolved, err := filepath.EvalSymlinks(claudeHome)
40+
if err == nil {
41+
claudeHome = resolved
42+
}
43+
3644
projectsDir := filepath.Join(claudeHome, "projects")
3745

3846
// Enumerate all .jsonl files under projects/<hash>/<session>.jsonl.
@@ -93,16 +101,16 @@ func FindActiveSessionPath(claudeHome string) (string, error) {
93101
return "", nil
94102
}
95103

96-
// findOpenFileWithLsof runs lsof -F n with a 3-second timeout and returns
97-
// the most recently modified path from jsonlFiles that appears in the lsof
98-
// output. When multiple JSONL files are open (e.g. stale FDs from previous
99-
// sessions), mtime selects the genuinely active one.
104+
// findOpenFileWithLsof runs lsof -c claude -F n with a 3-second timeout
105+
// and returns the most recently modified path from jsonlFiles that appears
106+
// in the lsof output. Scoped to Claude processes only (-c claude) to avoid
107+
// false positives from macOS Spotlight/mds indexing stale JSONL files.
100108
// Returns "" if lsof is unavailable, times out, or no match is found.
101109
func findOpenFileWithLsof(jsonlFiles []string) string {
102110
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
103111
defer cancel()
104112

105-
cmd := exec.CommandContext(ctx, "lsof", "-F", "n")
113+
cmd := exec.CommandContext(ctx, "lsof", "-c", "claude", "-F", "n")
106114
out, err := cmd.Output()
107115
if err != nil {
108116
// lsof failure is non-fatal; fall through to mtime heuristic.
@@ -115,7 +123,7 @@ func findOpenFileWithLsof(jsonlFiles []string) string {
115123
pathSet[p] = true
116124
}
117125

118-
// Collect all JSONL paths that lsof reports as open.
126+
// Collect all JSONL paths that lsof reports as open by Claude.
119127
var matches []string
120128
scanner := bufio.NewScanner(bytes.NewReader(out))
121129
for scanner.Scan() {
@@ -130,7 +138,6 @@ func findOpenFileWithLsof(jsonlFiles []string) string {
130138
}
131139

132140
// Among all matches, return the most recently modified file.
133-
// Old sessions can leave stale open FDs; the newest mtime is the live session.
134141
var bestPath string
135142
var bestMtime time.Time
136143
for _, p := range matches {

internal/claude/active_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func TestFindActiveSessionPath_EmptyProjectsDir(t *testing.T) {
5454
}
5555

5656
func TestFindActiveSessionPath_MtimeFallback_RecentFile(t *testing.T) {
57-
claudeHome := t.TempDir()
57+
claudeHome, _ := filepath.EvalSymlinks(t.TempDir())
5858
hashDir := filepath.Join(claudeHome, "projects", "abc123")
5959
if err := os.MkdirAll(hashDir, 0o755); err != nil {
6060
t.Fatal(err)

internal/mcp/tools.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,10 @@ func (s *Server) handleGetSessionStats(args json.RawMessage) (any, error) {
206206
if err == nil && meta != nil {
207207
// Step 3: build and return the live result. Do NOT write to the DB.
208208
cost := analyzer.EstimateSessionCost(*meta, pricing, ratio)
209+
tags := s.loadTags()
209210
return SessionStatsResult{
210211
SessionID: meta.SessionID,
211-
ProjectName: filepath.Base(meta.ProjectPath),
212+
ProjectName: resolveProjectName(meta.SessionID, meta.ProjectPath, tags),
212213
StartTime: meta.StartTime,
213214
DurationMin: meta.DurationMinutes, // 0 for live sessions — expected
214215
InputTokens: meta.InputTokens,

0 commit comments

Comments
 (0)