Skip to content

Commit ca50799

Browse files
committed
Attempt to fix a mem leak
1 parent 4493f40 commit ca50799

File tree

2 files changed

+70
-51
lines changed

2 files changed

+70
-51
lines changed

internal/plugins/worktree/agent.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,14 +691,14 @@ done
691691
if idx == -1 {
692692
continue
693693
}
694-
sessionName := part[:idx]
694+
sessionName := strings.Clone(part[:idx])
695695
content := ""
696696
if idx+3 < len(part) {
697697
content = part[idx+3:]
698698
// Trim leading newline from content
699699
content = strings.TrimPrefix(content, "\n")
700700
}
701-
results[sessionName] = content
701+
results[sessionName] = strings.Clone(content)
702702
}
703703

704704
return results, nil

internal/plugins/worktree/agent_session.go

Lines changed: 68 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import (
66
"crypto/sha256"
77
"encoding/hex"
88
"encoding/json"
9+
"io"
910
"os"
1011
"path/filepath"
1112
"sort"
1213
"strings"
1314
)
1415

16+
const sessionStatusTailBytes = 2 * 1024 * 1024
17+
1518
// detectAgentSessionStatus checks agent session files to determine if an agent
1619
// is waiting for user input or actively processing.
1720
// Returns StatusWaiting if last message is from assistant (agent finished, waiting for user).
@@ -222,44 +225,72 @@ func findMostRecentJSON(dir string, prefix string) (string, error) {
222225
return mostRecent, nil
223226
}
224227

225-
// getLastMessageStatusJSONL reads JSONL file and returns status based on last message.
226-
func getLastMessageStatusJSONL(path, typeField, userVal, assistantVal string) (WorktreeStatus, bool) {
228+
// readTailLines reads up to maxBytes from the end of a file and returns lines.
229+
// If the read starts mid-line, the first partial line is dropped.
230+
func readTailLines(path string, maxBytes int) ([]string, error) {
227231
file, err := os.Open(path)
228232
if err != nil {
229-
return 0, false
233+
return nil, err
230234
}
231235
defer file.Close()
232236

233-
var lastMsgType string
237+
info, err := file.Stat()
238+
if err != nil {
239+
return nil, err
240+
}
241+
size := info.Size()
242+
if size == 0 {
243+
return nil, nil
244+
}
234245

235-
scanner := bufio.NewScanner(file)
236-
buf := make([]byte, 64*1024)
237-
scanner.Buffer(buf, 1024*1024)
246+
start := int64(0)
247+
if size > int64(maxBytes) {
248+
start = size - int64(maxBytes)
249+
}
250+
if start > 0 {
251+
if _, err := file.Seek(start, io.SeekStart); err != nil {
252+
return nil, err
253+
}
254+
}
238255

239-
for scanner.Scan() {
256+
data, err := io.ReadAll(file)
257+
if err != nil {
258+
return nil, err
259+
}
260+
261+
lines := strings.Split(string(data), "\n")
262+
if start > 0 && len(lines) > 0 {
263+
lines = lines[1:]
264+
}
265+
return lines, nil
266+
}
267+
268+
// getLastMessageStatusJSONL reads JSONL file and returns status based on last message.
269+
func getLastMessageStatusJSONL(path, typeField, userVal, assistantVal string) (WorktreeStatus, bool) {
270+
lines, err := readTailLines(path, sessionStatusTailBytes)
271+
if err != nil {
272+
return 0, false
273+
}
274+
275+
for i := len(lines) - 1; i >= 0; i-- {
276+
line := strings.TrimSpace(lines[i])
277+
if line == "" {
278+
continue
279+
}
240280
var msg map[string]interface{}
241-
if err := json.Unmarshal(scanner.Bytes(), &msg); err != nil {
281+
if err := json.Unmarshal([]byte(line), &msg); err != nil {
242282
continue
243283
}
244284
if msgType, ok := msg[typeField].(string); ok {
245-
if msgType == userVal || msgType == assistantVal {
246-
lastMsgType = msgType
285+
switch msgType {
286+
case assistantVal:
287+
return StatusWaiting, true
288+
case userVal:
289+
return StatusActive, true
247290
}
248291
}
249292
}
250-
251-
if scanner.Err() != nil {
252-
return 0, false
253-
}
254-
255-
switch lastMsgType {
256-
case assistantVal:
257-
return StatusWaiting, true
258-
case userVal:
259-
return StatusActive, true
260-
default:
261-
return 0, false
262-
}
293+
return 0, false
263294
}
264295

265296
// findCodexSessionForPath finds the most recent Codex session matching CWD.
@@ -345,49 +376,37 @@ func cwdMatches(cwd, worktreePath string) bool {
345376

346377
// getCodexLastMessageStatus reads Codex JSONL and finds last message role.
347378
func getCodexLastMessageStatus(path string) (WorktreeStatus, bool) {
348-
file, err := os.Open(path)
379+
lines, err := readTailLines(path, sessionStatusTailBytes)
349380
if err != nil {
350381
return 0, false
351382
}
352-
defer file.Close()
353-
354-
var lastRole string
355383

356-
scanner := bufio.NewScanner(file)
357-
buf := make([]byte, 64*1024)
358-
scanner.Buffer(buf, 1024*1024)
359-
360-
for scanner.Scan() {
384+
for i := len(lines) - 1; i >= 0; i-- {
385+
line := strings.TrimSpace(lines[i])
386+
if line == "" {
387+
continue
388+
}
361389
var record struct {
362390
Type string `json:"type"`
363391
Payload struct {
364392
Type string `json:"type"`
365393
Role string `json:"role"`
366394
} `json:"payload"`
367395
}
368-
if err := json.Unmarshal(scanner.Bytes(), &record); err != nil {
396+
if err := json.Unmarshal([]byte(line), &record); err != nil {
369397
continue
370398
}
371399
// Codex uses type="response_item" with payload.type="message"
372400
if record.Type == "response_item" && record.Payload.Type == "message" {
373-
if record.Payload.Role == "user" || record.Payload.Role == "assistant" {
374-
lastRole = record.Payload.Role
401+
switch record.Payload.Role {
402+
case "assistant":
403+
return StatusWaiting, true
404+
case "user":
405+
return StatusActive, true
375406
}
376407
}
377408
}
378-
379-
if scanner.Err() != nil {
380-
return 0, false
381-
}
382-
383-
switch lastRole {
384-
case "assistant":
385-
return StatusWaiting, true
386-
case "user":
387-
return StatusActive, true
388-
default:
389-
return 0, false
390-
}
409+
return 0, false
391410
}
392411

393412
// getGeminiLastMessageStatus reads Gemini JSON session file.

0 commit comments

Comments
 (0)