@@ -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.
347378func 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