Skip to content

Commit f66fe02

Browse files
authored
Merge pull request #500 from bborn/task/2003-fix-daemon-killing-active-tasks-via-subs
Fix daemon killing active tasks via substring match
2 parents 82b2d9d + 7eeb31f commit f66fe02

File tree

2 files changed

+139
-42
lines changed

2 files changed

+139
-42
lines changed

internal/executor/executor.go

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -427,21 +427,15 @@ func (e *Executor) IsSuspended(taskID int64) bool {
427427
return suspended
428428
}
429429

430-
// getClaudePID finds the PID of the Claude process for a task.
431-
// It first checks the stored daemon session, then searches all sessions for the task window.
432-
func (e *Executor) getClaudePID(taskID int64) int {
433-
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
434-
defer cancel()
435-
436-
windowName := TmuxWindowName(taskID)
437-
438-
// Search all tmux sessions for a window with this task's name
439-
out, err := exec.CommandContext(ctx, "tmux", "list-panes", "-a", "-F", "#{session_name}:#{window_name}:#{pane_index} #{pane_pid}").Output()
440-
if err != nil {
441-
return 0
442-
}
443-
444-
for _, line := range strings.Split(string(out), "\n") {
430+
// findPanesForWindow parses tmux list-panes output and returns PIDs for panes
431+
// in windows matching the given name exactly. The input format is one line per pane:
432+
//
433+
// "session:window:pane pid"
434+
//
435+
// e.g. "task-daemon-123:task-5:0 12345"
436+
func findPanesForWindow(tmuxOutput, windowName string) []int {
437+
var pids []int
438+
for _, line := range strings.Split(tmuxOutput, "\n") {
445439
line = strings.TrimSpace(line)
446440
if line == "" {
447441
continue
@@ -456,16 +450,39 @@ func (e *Executor) getClaudePID(taskID int64) int {
456450
target := parts[0]
457451
pidStr := parts[1]
458452

459-
// Only match panes in windows named after this task
460-
if !strings.Contains(target, windowName) {
453+
// Parse target format: "session:window:pane" and match window name exactly
454+
targetParts := strings.SplitN(target, ":", 3)
455+
if len(targetParts) < 2 {
456+
continue
457+
}
458+
if targetParts[1] != windowName {
461459
continue
462460
}
463461

464462
pid, err := strconv.Atoi(pidStr)
465463
if err != nil {
466464
continue
467465
}
466+
pids = append(pids, pid)
467+
}
468+
return pids
469+
}
470+
471+
// getClaudePID finds the PID of the Claude process for a task.
472+
// It first checks the stored daemon session, then searches all sessions for the task window.
473+
func (e *Executor) getClaudePID(taskID int64) int {
474+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
475+
defer cancel()
476+
477+
windowName := TmuxWindowName(taskID)
468478

479+
// Search all tmux sessions for a window with this task's name
480+
out, err := exec.CommandContext(ctx, "tmux", "list-panes", "-a", "-F", "#{session_name}:#{window_name}:#{pane_index} #{pane_pid}").Output()
481+
if err != nil {
482+
return 0
483+
}
484+
485+
for _, pid := range findPanesForWindow(string(out), windowName) {
469486
// Check if this is a Claude process or has Claude as child
470487
cmdOut, _ := exec.CommandContext(ctx, "ps", "-p", strconv.Itoa(pid), "-o", "comm=").Output()
471488
if strings.Contains(string(cmdOut), "claude") {
@@ -5093,31 +5110,7 @@ func (e *Executor) getPiPID(taskID int64) int {
50935110
return 0
50945111
}
50955112

5096-
for _, line := range strings.Split(string(out), "\n") {
5097-
line = strings.TrimSpace(line)
5098-
if line == "" {
5099-
continue
5100-
}
5101-
5102-
// Parse "session:window:pane pid"
5103-
parts := strings.Fields(line)
5104-
if len(parts) != 2 {
5105-
continue
5106-
}
5107-
5108-
target := parts[0]
5109-
pidStr := parts[1]
5110-
5111-
// Only match panes in windows named after this task
5112-
if !strings.Contains(target, windowName) {
5113-
continue
5114-
}
5115-
5116-
pid, err := strconv.Atoi(pidStr)
5117-
if err != nil {
5118-
continue
5119-
}
5120-
5113+
for _, pid := range findPanesForWindow(string(out), windowName) {
51215114
// Check if this is a Pi process or has Pi as child
51225115
cmdOut, _ := exec.CommandContext(ctx, "ps", "-p", strconv.Itoa(pid), "-o", "comm=").Output()
51235116
if strings.Contains(string(cmdOut), "pi") || strings.Contains(string(cmdOut), "node") || strings.Contains(string(cmdOut), "ty") {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package executor
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestFindPanesForWindow(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
input string
11+
windowName string
12+
wantPIDs []int
13+
}{
14+
{
15+
name: "exact match",
16+
input: "task-daemon-123:task-5:0 12345",
17+
windowName: "task-5",
18+
wantPIDs: []int{12345},
19+
},
20+
{
21+
name: "no substring match on longer window name",
22+
input: "task-daemon-123:task-55:0 12345",
23+
windowName: "task-5",
24+
wantPIDs: nil,
25+
},
26+
{
27+
name: "no substring match on prefix range",
28+
input: "task-daemon-123:task-50:0 11111\ntask-daemon-123:task-55:0 22222\ntask-daemon-123:task-59:0 33333",
29+
windowName: "task-5",
30+
wantPIDs: nil,
31+
},
32+
{
33+
name: "match among multiple windows",
34+
input: "task-daemon-123:task-5:0 11111\ntask-daemon-123:task-55:0 22222\ntask-daemon-123:task-6:0 33333",
35+
windowName: "task-5",
36+
wantPIDs: []int{11111},
37+
},
38+
{
39+
name: "multiple panes in same window",
40+
input: "task-daemon-123:task-5:0 11111\ntask-daemon-123:task-5:1 22222",
41+
windowName: "task-5",
42+
wantPIDs: []int{11111, 22222},
43+
},
44+
{
45+
name: "no match",
46+
input: "task-daemon-123:task-10:0 12345",
47+
windowName: "task-5",
48+
wantPIDs: nil,
49+
},
50+
{
51+
name: "empty input",
52+
input: "",
53+
windowName: "task-5",
54+
wantPIDs: nil,
55+
},
56+
{
57+
name: "malformed line no colon",
58+
input: "nocolon 12345",
59+
windowName: "task-5",
60+
wantPIDs: nil,
61+
},
62+
{
63+
name: "malformed line no pid",
64+
input: "task-daemon-123:task-5:0",
65+
windowName: "task-5",
66+
wantPIDs: nil,
67+
},
68+
{
69+
name: "malformed pid non-numeric",
70+
input: "task-daemon-123:task-5:0 notapid",
71+
windowName: "task-5",
72+
wantPIDs: nil,
73+
},
74+
{
75+
name: "whitespace and empty lines",
76+
input: " \n\ntask-daemon-123:task-5:0 12345\n \n",
77+
windowName: "task-5",
78+
wantPIDs: []int{12345},
79+
},
80+
{
81+
name: "session name contains window name but window differs",
82+
input: "task-daemon-task-5:task-55:0 12345",
83+
windowName: "task-5",
84+
wantPIDs: nil,
85+
},
86+
}
87+
88+
for _, tt := range tests {
89+
t.Run(tt.name, func(t *testing.T) {
90+
got := findPanesForWindow(tt.input, tt.windowName)
91+
if len(got) == 0 && len(tt.wantPIDs) == 0 {
92+
return // both empty/nil
93+
}
94+
if len(got) != len(tt.wantPIDs) {
95+
t.Fatalf("findPanesForWindow() returned %d PIDs, want %d\n got: %v\n want: %v", len(got), len(tt.wantPIDs), got, tt.wantPIDs)
96+
}
97+
for i, pid := range got {
98+
if pid != tt.wantPIDs[i] {
99+
t.Errorf("PID[%d] = %d, want %d", i, pid, tt.wantPIDs[i])
100+
}
101+
}
102+
})
103+
}
104+
}

0 commit comments

Comments
 (0)