Skip to content

fix: adaptive throttle for timer-based status updates#52

Open
sunchan-park wants to merge 1 commit intosix-ddc:mainfrom
sunchan-park:fix/adaptive-status-throttle
Open

fix: adaptive throttle for timer-based status updates#52
sunchan-park wants to merge 1 commit intosix-ddc:mainfrom
sunchan-park:fix/adaptive-status-throttle

Conversation

@sunchan-park
Copy link
Contributor

Problem

When Claude Code runs a long task (e.g. waiting for an agent sub-task), the terminal status line shows a timer that increments every second:

✻ Thinking… 5s
✶ Coalescing… (25m 8s · ↓ 5.8k tokens · thought for 6s)
✢ Drizzling… (54s · ↓ 776 tokens)

The status poller (STATUS_POLL_INTERVAL = 1s) captures each tick as a new text change and enqueues an edit_message_text call to Telegram — roughly 60 calls/min. This quickly exceeds Telegram's rate limits, triggering 429 Too Many Requests responses.

The existing AIORateLimiter(max_retries=5) and flood-control logic handle the response but don't reduce the request rate, so the cycle repeats:

  1. Bot sends ~60 edits/min → Telegram returns 429
  2. Flood control pauses the bot for the ban duration
  3. Ban expires → bot immediately resumes 60 edits/min → 429 again
  4. Repeat all night → bot becomes effectively silent

In practice, this caused the bot to go completely unresponsive overnight while Claude Code was running long agent tasks. All queued messages arrived at once the next morning when the ban finally lifted.

Solution

Add _should_send_status() in status_polling.py that detects timer suffixes via regex and applies an adaptive send interval based on how long the same status has been active:

Elapsed time Send interval Telegram API calls
0–10 s every 1 s (real-time) ~60/min
10–60 s every 5 s ~12/min
60 s+ every 30 s ~2/min

Timer detection

Claude Code uses two status line formats with timers:

Thinking… 5s                                              ← bare suffix
Coalescing… (25m 8s · ↓ 5.8k tokens · thought for 6s)    ← parenthesized with metadata
Drizzling… (54s · ↓ 776 tokens)                           ← parenthesized short

The regex matches both patterns and extracts the "base text" (everything before the timer) for comparison — so if the task description changes (different base text), the throttle resets instantly and the new status sends immediately.

Key design decisions

  • Poll interval stays at 1 s — interactive UI detection (permission prompts, etc.) is completely unaffected
  • Non-timer status changes always send immediately — e.g. "Reading file" → "Writing file" is never throttled
  • Base text comparison — only the timer portion is ignored; if the task changes, throttle resets

Configuration

Intervals are configurable via STATUS_THROTTLE_INTERVALS env var (comma-separated seconds for the three elapsed-time tiers):

# Default
STATUS_THROTTLE_INTERVALS=1,5,30

# Disable throttling entirely
STATUS_THROTTLE_INTERVALS=1,1,1

# More aggressive throttling
STATUS_THROTTLE_INTERVALS=1,10,60

Changes

File Change
src/ccbot/config.py Add status_throttle_intervals config with STATUS_THROTTLE_INTERVALS env var
src/ccbot/handlers/status_polling.py Add _should_send_status() adaptive throttle, call before enqueue_status_update()
tests/ccbot/handlers/test_timer_throttle.py 31 tests — regex patterns, throttle logic, config integration
tests/ccbot/test_config.py 6 tests — config parsing and validation

Test plan

  • All 273 tests pass
  • Regex matches bare timers: 5s, 1m 30s, 2m, 5s (Esc to interrupt)
  • Regex matches parenthesized timers: (54s · ↓ 776 tokens), (25m 8s · ↓ 5.8k · …)
  • Regex rejects non-timer text: Reading file.py, file2s, Bash echo hello
  • Base text extraction correct for both formats
  • Real-time updates in first 10 s, throttled to 5 s / 30 s after
  • Base text change resets throttle immediately
  • Non-timer status always passes through
  • Custom intervals via STATUS_THROTTLE_INTERVALS work
  • Invalid env var values fall back to defaults with warning log
  • Independent throttle state per (user_id, thread_id)
  • Tested against live Claude Code status lines (Coalescing…, Drizzling…)

🤖 Generated with Claude Code

…am rate limits

When Claude Code waits on a long-running task (e.g. agent sub-tasks),
the status line timer increments every second ("Thinking… 1s", "2s", …).
The status poller detects each tick as a text change and enqueues a
Telegram edit_message_text call — roughly 60/min.  This quickly exceeds
Telegram's rate limit, triggering 429 responses and a flood-control ban
that silences the bot for extended periods.

Add _should_send_status() in status_polling.py that detects timer
suffixes via regex and applies an adaptive send interval based on how
long the same status has been active:

  0–10 s  → every 1 s  (real-time, useful for short tasks)
  10–60 s → every 5 s
  60 s+   → every 30 s

Key design decisions:
- Poll interval stays at 1 s — interactive UI detection is unaffected
- Non-timer status changes always send immediately (no delay)
- Timer detection uses a trailing regex matching "5s", "1m 30s", etc.
- Intervals are configurable via STATUS_THROTTLE_INTERVALS env var
  (comma-separated, e.g. "1,5,30"; set "1,1,1" to disable)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant