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:
dj-oyu 2026-03-20 04:12:36 +09:00
parent 8136dc79cc
commit 8ef7c41ad8
3 changed files with 25 additions and 8 deletions

View file

@ -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(),
"stderr": strings.Join(stderrTail, "\n"),
})
return fmt.Sprintf("[file:%s]", pdfPath)
}

View file

@ -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 {
if err := editor.EditMessage(ctx, msg.ChatID, entry.id, msg.Content); err == nil {
// 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 {
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 {
if err := editor.EditMessage(ctx, msg.ChatID, entry.messageID, msg.Content); err == nil {
m.statusEditTimes.Store(key, time.Now())
if err := editor.EditMessage(ctx, msg.ChatID, entry.messageID, msg.Content); err == nil {
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 {
if err := editor.EditMessage(ctx, msg.ChatID, entry.messageID, msg.Content); err == nil {
m.statusEditTimes.Store(taskKey, time.Now())
if err := editor.EditMessage(ctx, msg.ChatID, entry.messageID, msg.Content); err == nil {
return
}
}

View file

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