Skip to content
Open
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
Binary file added cagent
Binary file not shown.
43 changes: 42 additions & 1 deletion pkg/app/transcript/transcript.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,49 @@ import (
func PlainText(sess *session.Session) string {
var builder strings.Builder

// Make a copy of the session items to avoid race conditions
// Messages is a public field, so we can access it directly
items := make([]session.Item, len(sess.Messages))
copy(items, sess.Messages)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 HIGH: Race condition accessing sess.Messages without lock

The code copies sess.Messages without holding the session lock, despite the comment claiming to "avoid race conditions."

Problem: The Session type has a mu sync.RWMutex that protects Messages from concurrent read/write access. The existing GetAllMessages() method properly acquires s.mu.RLock() before reading Messages. This new code directly accesses sess.Messages without acquiring the lock.

Impact:

  • Data races detectable by Go's race detector (go test -race)
  • Potential panics if the slice is reallocated during concurrent writes
  • Corrupted or inconsistent transcript output

Fix: Acquire the session lock before accessing Messages:

sess.mu.RLock()
items := make([]session.Item, len(sess.Messages))
copy(items, sess.Messages)
sess.mu.RUnlock()

Or better yet, reuse the existing GetAllMessages() which already handles locking correctly.


// Find the last summary in the session
lastSummaryIndex := -1
var summary string
for i := len(items) - 1; i >= 0; i-- {
if items[i].Summary != "" {
lastSummaryIndex = i
summary = items[i].Summary
break
}
}

// If a summary exists, start with it
if lastSummaryIndex >= 0 {
fmt.Fprintf(&builder, "## Session Summary\n\n%s\n", summary)
}

// Get all messages
messages := sess.GetAllMessages()
for i := range messages {

// If we have a summary, we need to skip messages that were summarized
// We do this by tracking message indices and only including messages after the summary
var startMessageIndex int
if lastSummaryIndex >= 0 {
// Count how many messages come before the summary
messageCount := 0
for i := 0; i <= lastSummaryIndex; i++ {
if items[i].IsMessage() {
messageCount++
} else if items[i].IsSubSession() {
// Count all messages in the sub-session
messageCount += len(items[i].SubSession.GetAllMessages())
}
}
startMessageIndex = messageCount
}

// Write messages (starting after the summary if one exists)
for i := startMessageIndex; i < len(messages); i++ {
msg := messages[i]

if msg.Implicit {
Expand Down
Loading