fix: prevent Telegram 429 rate limit errors during PDF OCR
Three fixes for the rate limit storm during long processing operations: 1. Increase statusEditInterval from 500ms to 3s — Telegram enforces ~20 messages/min per group chat, so 3s is safe. 2. Update statusEditTimes before calling EditMessage (not only on success). Previously, failed edits (429) didn't update the throttle timestamp, causing immediate retries on the next tick. 3. Skip parse-mode retry on 429 errors — rate limit errors are not parse failures, so retrying with plain text doubles the request count for no benefit. Also increase the processingIndicator spinner tick from 150ms to 3s to match the throttle interval — sub-second spinners are unnecessary for multi-second operations like PDF OCR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8136dc79cc
commit
8ef7c41ad8
3 changed files with 25 additions and 8 deletions
|
|
@ -372,7 +372,7 @@ func (al *AgentLoop) processingIndicator(ctx context.Context, channel, chatID, l
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(150 * time.Millisecond)
|
ticker := time.NewTicker(3 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
frame := 0
|
frame := 0
|
||||||
|
|
@ -596,8 +596,10 @@ func (al *AgentLoop) ocrPDF(
|
||||||
return fmt.Sprintf("[file:%s]", pdfPath)
|
return fmt.Sprintf("[file:%s]", pdfPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track progress via stderr
|
// Track progress via stderr, keep last lines for error diagnosis
|
||||||
page := 0
|
page := 0
|
||||||
|
var stderrTail []string
|
||||||
|
const maxTailLines = 20
|
||||||
scanner := bufio.NewScanner(stderrPipe)
|
scanner := bufio.NewScanner(stderrPipe)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
|
|
@ -605,12 +607,17 @@ func (al *AgentLoop) ocrPDF(
|
||||||
page++
|
page++
|
||||||
indicator.UpdateLabel(fmt.Sprintf("%s (%d/%s)...", modeLabel, page, totalStr))
|
indicator.UpdateLabel(fmt.Sprintf("%s (%d/%s)...", modeLabel, page, totalStr))
|
||||||
}
|
}
|
||||||
|
stderrTail = append(stderrTail, line)
|
||||||
|
if len(stderrTail) > maxTailLines {
|
||||||
|
stderrTail = stderrTail[1:]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if waitErr := cmd.Wait(); waitErr != nil {
|
if waitErr := cmd.Wait(); waitErr != nil {
|
||||||
logger.WarnCF("agent", "OCR command failed", map[string]any{
|
logger.WarnCF("agent", "OCR command failed", map[string]any{
|
||||||
"path": pdfPath,
|
"path": pdfPath,
|
||||||
"error": waitErr.Error(),
|
"error": waitErr.Error(),
|
||||||
|
"stderr": strings.Join(stderrTail, "\n"),
|
||||||
})
|
})
|
||||||
return fmt.Sprintf("[file:%s]", pdfPath)
|
return fmt.Sprintf("[file:%s]", pdfPath)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ const (
|
||||||
// for the same status/task bubble. EditMessage APIs are more rate-sensitive
|
// for the same status/task bubble. EditMessage APIs are more rate-sensitive
|
||||||
// than SendMessageDraft, so we throttle edits to avoid "(edited)" flicker
|
// than SendMessageDraft, so we throttle edits to avoid "(edited)" flicker
|
||||||
// and API rate limit errors. Draft-based channels bypass this throttle.
|
// and API rate limit errors. Draft-based channels bypass this throttle.
|
||||||
statusEditInterval = 500 * time.Millisecond
|
// Telegram enforces ~20 messages/min per group chat, so 3s is safe.
|
||||||
|
statusEditInterval = 3 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// statusMsgEntry tracks a status or task message ID for later editing.
|
// statusMsgEntry tracks a status or task message ID for later editing.
|
||||||
|
|
@ -85,8 +86,9 @@ func (m *Manager) handleStatusSend(ctx context.Context, name string, w *channelW
|
||||||
if v, loaded := m.placeholders.Load(key); loaded {
|
if v, loaded := m.placeholders.Load(key); loaded {
|
||||||
if entry, ok := v.(placeholderEntry); ok && entry.id != "" {
|
if entry, ok := v.(placeholderEntry); ok && entry.id != "" {
|
||||||
if editor, ok := w.ch.(MessageEditor); ok {
|
if editor, ok := w.ch.(MessageEditor); ok {
|
||||||
|
// Always record edit time to prevent retry storms on 429 errors.
|
||||||
|
m.statusEditTimes.Store(key, time.Now())
|
||||||
if err := editor.EditMessage(ctx, msg.ChatID, entry.id, msg.Content); err == nil {
|
if err := editor.EditMessage(ctx, msg.ChatID, entry.id, msg.Content); err == nil {
|
||||||
m.statusEditTimes.Store(key, time.Now())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -97,8 +99,8 @@ func (m *Manager) handleStatusSend(ctx context.Context, name string, w *channelW
|
||||||
if v, loaded := m.statusMsgIDs.Load(key); loaded {
|
if v, loaded := m.statusMsgIDs.Load(key); loaded {
|
||||||
if entry, ok := v.(statusMsgEntry); ok && entry.messageID != "" {
|
if entry, ok := v.(statusMsgEntry); ok && entry.messageID != "" {
|
||||||
if editor, ok := w.ch.(MessageEditor); ok {
|
if editor, ok := w.ch.(MessageEditor); ok {
|
||||||
|
m.statusEditTimes.Store(key, time.Now())
|
||||||
if err := editor.EditMessage(ctx, msg.ChatID, entry.messageID, msg.Content); err == nil {
|
if err := editor.EditMessage(ctx, msg.ChatID, entry.messageID, msg.Content); err == nil {
|
||||||
m.statusEditTimes.Store(key, time.Now())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -223,8 +225,8 @@ func (m *Manager) handleTaskStatusSend(ctx context.Context, name string, w *chan
|
||||||
if v, loaded := m.taskMsgIDs.Load(taskKey); loaded {
|
if v, loaded := m.taskMsgIDs.Load(taskKey); loaded {
|
||||||
if entry, ok := v.(statusMsgEntry); ok && entry.messageID != "" {
|
if entry, ok := v.(statusMsgEntry); ok && entry.messageID != "" {
|
||||||
if editor, ok := w.ch.(MessageEditor); ok {
|
if editor, ok := w.ch.(MessageEditor); ok {
|
||||||
|
m.statusEditTimes.Store(taskKey, time.Now())
|
||||||
if err := editor.EditMessage(ctx, msg.ChatID, entry.messageID, msg.Content); err == nil {
|
if err := editor.EditMessage(ctx, msg.ChatID, entry.messageID, msg.Content); err == nil {
|
||||||
m.statusEditTimes.Store(taskKey, time.Now())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,10 @@ func (c *TelegramChannel) sendChunk(
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := c.bot.SendMessage(ctx, tgMsg); err != nil {
|
if _, err := c.bot.SendMessage(ctx, tgMsg); err != nil {
|
||||||
|
// Don't retry on rate limit errors — they aren't parse failures.
|
||||||
|
if strings.Contains(err.Error(), "429") || strings.Contains(err.Error(), "Too Many Requests") {
|
||||||
|
return fmt.Errorf("telegram send: %w", channels.ErrTemporary)
|
||||||
|
}
|
||||||
logParseFailed(err, params.useMarkdownV2)
|
logParseFailed(err, params.useMarkdownV2)
|
||||||
|
|
||||||
tgMsg.Text = params.mdFallback
|
tgMsg.Text = params.mdFallback
|
||||||
|
|
@ -372,6 +376,10 @@ func (c *TelegramChannel) EditMessage(ctx context.Context, chatID string, messag
|
||||||
}
|
}
|
||||||
_, err = c.bot.EditMessageText(ctx, editMsg)
|
_, err = c.bot.EditMessageText(ctx, editMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Don't retry on rate limit errors — they aren't parse failures.
|
||||||
|
if strings.Contains(err.Error(), "429") || strings.Contains(err.Error(), "Too Many Requests") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
logParseFailed(err, useMarkdownV2)
|
logParseFailed(err, useMarkdownV2)
|
||||||
_, err = c.bot.EditMessageText(ctx, tu.EditMessageText(tu.ID(cid), mid, content))
|
_, err = c.bot.EditMessageText(ctx, tu.EditMessageText(tu.ID(cid), mid, content))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue