diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 44357162a..314ca07cc 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -169,7 +169,14 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err limit = 4000 // Default Telegram limit (~4096) } - chunks := utils.SplitMessage(msg.Content, limit) + // Telegram HTML tags (like
) can expand content significantly.
+	// We use a safe headroom for the markdown-based split.
+	effectiveMarkdownLimit := limit - 500
+	if effectiveMarkdownLimit < 500 {
+		effectiveMarkdownLimit = 500
+	}
+
+	chunks := utils.SplitMessage(msg.Content, effectiveMarkdownLimit)
 
 	for i, chunk := range chunks {
 		htmlContent := markdownToTelegramHTML(chunk)
diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go
index 350607f57..8ea0c9b78 100644
--- a/pkg/tools/shell.go
+++ b/pkg/tools/shell.go
@@ -290,9 +290,9 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
 			return ""
 		}
 
-		// Match absolute paths: Unix (starts with / after space/start/quotes) or Windows (X:\)
+		// Match absolute paths: Unix (starts with / after space/start/quotes/=/:) or Windows (X:\)
 		// This regex is careful not to match scoped packages like @mastra/core
-		pathPattern := regexp.MustCompile(`(?:\s|^|["'|&;])(/[^\s\"'|&;]+)|([A-Za-z]:\\[^\\\"'|&;]+)`)
+		pathPattern := regexp.MustCompile(`(?:\s|^|["'|&;=:])(/[^\s\"'|&;:]+)|([A-Za-z]:\\[^\\\"'|&;:]+)`)
 		matches := pathPattern.FindAllStringSubmatch(cmd, -1)
 
 		for _, match := range matches {
diff --git a/pkg/utils/message.go b/pkg/utils/message.go
index 9caad88e8..45195fdd0 100644
--- a/pkg/utils/message.go
+++ b/pkg/utils/message.go
@@ -9,6 +9,13 @@ import (
 // but may extend to maxLen when needed. It respects rune counts to ensure multi-byte
 // characters (like emojis or CJK) are not split in half.
 func SplitMessage(content string, maxLen int) []string {
+	if content == "" {
+		return nil
+	}
+	if maxLen <= 0 {
+		return []string{content}
+	}
+
 	var messages []string
 
 	// Dynamic buffer: 10% of maxLen, but at least 50 chars if possible
@@ -35,7 +42,8 @@ func SplitMessage(content string, maxLen int) []string {
 		}
 
 		// Find natural split point within the effective limit
-		msgEnd := findLastSentenceBoundaryRunes(runes[:effectiveLimit], 300)
+		// We pass the full slice and a limit so findLastSentenceBoundaryRunes can look ahead.
+		msgEnd := findLastSentenceBoundaryRunes(runes, effectiveLimit, 300)
 		if msgEnd <= 0 {
 			msgEnd = findLastNewlineRunes(runes[:effectiveLimit], 200)
 		}
@@ -84,7 +92,7 @@ func SplitMessage(content string, maxLen int) []string {
 					if msgEnd > headerEnd+20 {
 						// Find a better split point closer to maxLen
 						innerLimit := maxLen - 5 // Leave room for "\n```"
-						betterEnd := findLastSentenceBoundaryRunes(runes[:innerLimit], 300)
+						betterEnd := findLastSentenceBoundaryRunes(runes, innerLimit, 300)
 						if betterEnd <= headerEnd {
 							betterEnd = findLastNewlineRunes(runes[:innerLimit], 200)
 						}
@@ -105,7 +113,7 @@ func SplitMessage(content string, maxLen int) []string {
 					}
 
 					// Otherwise, try to split before the code block starts
-					newEnd := findLastSentenceBoundaryRunes(runes[:unclosedIdx], 300)
+					newEnd := findLastSentenceBoundaryRunes(runes, unclosedIdx, 300)
 					if newEnd <= 0 {
 						newEnd = findLastNewlineRunes(runes[:unclosedIdx], 200)
 					}
@@ -210,21 +218,27 @@ func findLastSpaceRunes(runes []rune, searchWindow int) int {
 	return -1
 }
 
-// findLastSentenceBoundaryRunes finds the last sentence-ending punctuation
-// Returns the position after the punctuation or -1 if not found
-func findLastSentenceBoundaryRunes(runes []rune, searchWindow int) int {
-	searchStart := len(runes) - searchWindow
+// findLastSentenceBoundaryRunes finds the last sentence-ending punctuation within a limit.
+// It looks ahead to verify the boundary is real (followed by space, newline, or end of string).
+func findLastSentenceBoundaryRunes(runes []rune, limit int, searchWindow int) int {
+	if limit > len(runes) {
+		limit = len(runes)
+	}
+	searchStart := limit - searchWindow
 	if searchStart < 0 {
 		searchStart = 0
 	}
-	for i := len(runes) - 1; i >= searchStart; i-- {
+	for i := limit - 1; i >= searchStart; i-- {
 		switch runes[i] {
 		case '.', '!', '?', '。', '!', '?':
-			// Ensure it's the end of a sentence (followed by space, newline, or end of string)
-			if i == len(runes)-1 || runes[i+1] == ' ' || runes[i+1] == '\n' || runes[i+1] == '\t' {
+			// Ensure it's a true boundary: 
+			// either it's the very end of the full message, 
+			// or the NEXT rune (lookahead) is a space, newline, or tab.
+			if i == len(runes)-1 || (i+1 < len(runes) && (runes[i+1] == ' ' || runes[i+1] == '\n' || runes[i+1] == '\t')) {
 				return i + 1
 			}
 		}
 	}
 	return -1
 }
+
diff --git a/pkg/utils/message_test.go b/pkg/utils/message_test.go
index b99eb93ab..686d92d03 100644
--- a/pkg/utils/message_test.go
+++ b/pkg/utils/message_test.go
@@ -28,6 +28,12 @@ func TestSplitMessage(t *testing.T) {
 			maxLen:       2000,
 			expectChunks: 1,
 		},
+		{
+			name:         "MaxLen 0 (no split)",
+			content:      "Hello world",
+			maxLen:       0,
+			expectChunks: 1,
+		},
 		{
 			name:         "Simple split regular text",
 			content:      longText,
@@ -44,11 +50,6 @@ func TestSplitMessage(t *testing.T) {
 		},
 		{
 			name: "Split at newline",
-			// 1750 chars then newline, then more chars.
-			// Dynamic buffer: 2000 / 10 = 200.
-			// Effective limit: 2000 - 200 = 1800.
-			// Split should happen at newline because it's at 1750 (< 1800).
-			// Total length must > 2000 to trigger split. 1750 + 1 + 300 = 2051.
 			content:      strings.Repeat("a", 1750) + "\n" + strings.Repeat("b", 300),
 			maxLen:       2000,
 			expectChunks: 2,
@@ -67,11 +68,9 @@ func TestSplitMessage(t *testing.T) {
 			maxLen:       2000,
 			expectChunks: 2,
 			checkContent: func(t *testing.T, chunks []string) {
-				// Check that first chunk ends with closing fence
 				if !strings.HasSuffix(chunks[0], "\n```") {
 					t.Error("First chunk should end with injected closing fence")
 				}
-				// Check that second chunk starts with execution header
 				if !strings.HasPrefix(chunks[1], "```go") {
 					t.Error("Second chunk should start with injected code block header")
 				}
@@ -83,12 +82,20 @@ func TestSplitMessage(t *testing.T) {
 			maxLen:       2000,
 			expectChunks: 2,
 			checkContent: func(t *testing.T, chunks []string) {
-				// Just verify we didn't panic and got valid strings.
-				// Go strings are UTF-8, if we split mid-rune it would be bad,
-				// but standard slicing might do that.
-				// Let's assume standard behavior is acceptable or check if it produces invalid rune?
-				if !strings.Contains(chunks[0], "\u4e16") {
-					t.Error("Chunk should contain unicode characters")
+				// Each chunk should stay within max runes limit
+				for i, chunk := range chunks {
+					runeCount := len([]rune(chunk))
+					if runeCount > 2000 {
+						t.Errorf("Chunk %d has too many runes: %d", i, runeCount)
+					}
+				}
+				// Verify total rune count
+				totalRunes := 0
+				for _, chunk := range chunks {
+					totalRunes += len([]rune(chunk))
+				}
+				if totalRunes != 2500 {
+					t.Errorf("Total rune count mismatch. Got %d, want 2500", totalRunes)
 				}
 			},
 		},