rate-limit-safeguards — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited rate-limit-safeguards (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill documents the architecture and operational patterns for preventing API rate-limit crashes in automated AI agent task loops. The core insight: never inject tasks into a session that cannot respond. Instead, queue them and drain slowly on recovery.
Born from a real incident where a feedback loop of automated prompts into a rate-limited session extended the lockout period catastrophically. The queue system eliminates that class of failure.
Apply these safeguards whenever:
Tasks that cannot be injected immediately are stored in a JSON queue file and drained slowly once the session recovers. No task is ever lost -- just spaced out.
/tmp/task_queue.jsonQueue entry structure:
{"type": "task-type-name", "message": "...", "queued_at": 1743500000}Dedup by type: If the same task type is already in the queue, the duplicate is dropped silently. This prevents backlog explosion for recurring tasks.
session_is_responsive() uses two signals:
for your session. If modified within RECENT_THRESHOLD=300 seconds (5 minutes), session is considered active.
300-second threshold.
If either signal is fresh, the session is responsive. Both stale = unresponsive = queue mode.
Central routing function called by every scheduled task:
inject_or_queue "task-type" "$TASK_MESSAGE" "$SESSION"Decision tree:
SESSION_RESPONSIVE == true AND CYCLE_INJECTIONS < MAX_INJECTIONS_PER_CYCLE-> Acquire lock, inject via tmux send-keys, increment counter, release lock
-> queue_task() -- append to JSON queue, log "Queued task: <type>"
At the start of each cycle, if the session is responsive and the queue is non-empty:
drain_queue 2 30 "$SESSION"Parameters:
max_drain=2 -- drain at most 2 tasks per cyclespacing=30 -- wait 30 seconds between each drained taskqueued_at)CYCLE_INJECTIONS before any new injectionsMAX_INJECTIONS_PER_CYCLE=3Total injections (queue drain + new) per 30-minute cycle. This prevents API overload when multiple tasks become due simultaneously (e.g., after a 2-hour outage clears).
Budget allocation example:
When queue size reaches 10+ items, an alert is sent to the operator via your preferred notification channel (Telegram, Slack, email, etc.) once per alert period. Use a guard file to prevent repeated alerts. Alert resets when queue drops below 10.
| Variable | Value | Purpose |
|---|---|---|
TASK_QUEUE_FILE | /tmp/task_queue.json | Queue storage |
RECENT_THRESHOLD | 300 (5 min) | Responsiveness window |
MAX_INJECTIONS_PER_CYCLE | 3 | Per-cycle injection cap |
LOCK_TTL | 900 (15 min) | Lock staleness threshold |
| Queue alert threshold | 10 items | Alert trigger |
| Drain per cycle | 2 | Max queue drain per cycle |
| Drain spacing | 30s | Delay between drained tasks |
| Function | Purpose |
|---|---|
session_is_responsive() | Returns 0 if session active, 1 if stale |
inject_or_queue(type, msg, session) | Routes task to injection or queue |
queue_task(type, message) | Appends to JSON queue with dedup |
drain_queue(max, spacing, session) | Drains N oldest tasks with spacing |
queue_size() | Returns current queue length |
acquire_lock(name) | Creates lock file. Returns 1 if held. |
release_lock(name) | Removes lock file (caller must be holder) |
Prevents multiple simultaneous tmux injections:
/tmp/task_inject_lockLOCK_TTL=900)task from permanently blocking the queue)
inject_or_queue handles lock internally. Callers do NOT call acquire/release directly.These are separate from the task queue but address the same root cause: API overload from too many concurrent requests.
| Scenario | Approach |
|---|---|
| 1-2 independent agents | Parallel is fine |
| 3+ agents | Sequential -- wait for each to complete before spawning next |
| Background agents | Max 2 concurrent. Use run_in_background: true. |
| After spawning background agent | Wait at least 30 seconds before spawning another |
Principle: Prefer sequential spawning with spacing over concurrent hard caps. Spacing prevents API burst without sacrificing throughput on single-agent tasks.
Root cause: Automated prompts kept firing into a rate-limited session.
Mechanism:
Fix applied: The queue system prevents injection entirely when the session is unresponsive. Tasks accumulate in JSON without touching the API. When the session recovers, they drain slowly (2 per cycle, 30s spacing, under the 3-injection cap), staying well under per-minute limits.
When rate-limited:
without injecting them. No manual intervention needed.
tasks with 30-second spacing, respecting the 3-injection cap.
They can assess whether manual intervention is needed.
over subsequent cycles.
fires and the operator determines the backlog is stale/irrelevant (in which case: echo "[]" > /tmp/task_queue.json).
Watch for these early warning signals:
python3 -c "import json; print(len(json.load(open('/tmp/task_queue.json'))))")To adopt this pattern in your own AI agent setup:
session_is_responsive()queue_task()drain_queue()queue_size()inject_or_queue()acquire_lock() / release_lock()tmux send-keys injection calls with inject_or_queue "type" "$MSG" "$SESSION" SESSION_RESPONSIVE="false"
CYCLE_INJECTIONS=0
if session_is_responsive; then
SESSION_RESPONSIVE="true"
Q_SIZE=$(queue_size)
if [[ $Q_SIZE -gt 0 ]]; then
DRAINED=$(drain_queue 2 30 "$SESSION")
CYCLE_INJECTIONS=$((CYCLE_INJECTIONS + DRAINED))
fi
fi~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.