Refine message chunking and shell safety guard based on Copilot review

This commit is contained in:
Andrew 2026-02-23 10:16:25 +00:00
parent 43609ca958
commit 2e84df5e92
4 changed files with 54 additions and 26 deletions

View file

@ -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 <pre><code>) 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)

View file

@ -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 {

View file

@ -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
}

View file

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