From 8ef7c41ad81ed7b5aff675187a814d017022d627 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:12:36 +0900 Subject: [PATCH] fix: prevent Telegram 429 rate limit errors during PDF OCR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- pkg/agent/loop_media.go | 15 +++++++++++---- pkg/channels/manager_ext.go | 10 ++++++---- pkg/channels/telegram/telegram.go | 8 ++++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/pkg/agent/loop_media.go b/pkg/agent/loop_media.go index 9ab243bcc..766dc5815 100644 --- a/pkg/agent/loop_media.go +++ b/pkg/agent/loop_media.go @@ -372,7 +372,7 @@ func (al *AgentLoop) processingIndicator(ctx context.Context, channel, chatID, l } go func() { - ticker := time.NewTicker(150 * time.Millisecond) + ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() frame := 0 @@ -596,8 +596,10 @@ func (al *AgentLoop) ocrPDF( return fmt.Sprintf("[file:%s]", pdfPath) } - // Track progress via stderr + // Track progress via stderr, keep last lines for error diagnosis page := 0 + var stderrTail []string + const maxTailLines = 20 scanner := bufio.NewScanner(stderrPipe) for scanner.Scan() { line := scanner.Text() @@ -605,12 +607,17 @@ func (al *AgentLoop) ocrPDF( page++ 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 { logger.WarnCF("agent", "OCR command failed", map[string]any{ - "path": pdfPath, - "error": waitErr.Error(), + "path": pdfPath, + "error": waitErr.Error(), + "stderr": strings.Join(stderrTail, "\n"), }) return fmt.Sprintf("[file:%s]", pdfPath) } diff --git a/pkg/channels/manager_ext.go b/pkg/channels/manager_ext.go index 0c84cd07f..0c8cb5d16 100644 --- a/pkg/channels/manager_ext.go +++ b/pkg/channels/manager_ext.go @@ -18,7 +18,8 @@ const ( // for the same status/task bubble. EditMessage APIs are more rate-sensitive // than SendMessageDraft, so we throttle edits to avoid "(edited)" flicker // 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. @@ -85,8 +86,9 @@ func (m *Manager) handleStatusSend(ctx context.Context, name string, w *channelW if v, loaded := m.placeholders.Load(key); loaded { if entry, ok := v.(placeholderEntry); ok && entry.id != "" { 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 { - m.statusEditTimes.Store(key, time.Now()) 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 entry, ok := v.(statusMsgEntry); ok && entry.messageID != "" { 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 { - m.statusEditTimes.Store(key, time.Now()) 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 entry, ok := v.(statusMsgEntry); ok && entry.messageID != "" { 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 { - m.statusEditTimes.Store(taskKey, time.Now()) return } } diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index d666b4b03..2e8fe2a75 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -305,6 +305,10 @@ func (c *TelegramChannel) sendChunk( } 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) tgMsg.Text = params.mdFallback @@ -372,6 +376,10 @@ func (c *TelegramChannel) EditMessage(ctx context.Context, chatID string, messag } _, err = c.bot.EditMessageText(ctx, editMsg) 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) _, err = c.bot.EditMessageText(ctx, tu.EditMessageText(tu.ID(cid), mid, content)) }