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)
}
},
},