Documentation menu
Getting started
New in v2.0
Feature deep dives
Reference
Troubleshooting
Troubleshooting runbook
One failure class at a time: unhealthy services, a login loop, a task that never forwards, a stuck queue, FloodWait, busy ports and truncated logs.
Verified against the checkout built for v2.2.4 · updated 20 Oct 2026
On this page
This is the page you open when something is wrong. Every section names the symptom as you meet it, the cause in the implementation, the exact commands to run and the evidence that proves the fix. Nothing here asks you to guess.
Fast triage
Four commands, cheapest first, tell you whether the failure is the stack, the configuration or the rules. Run them before reading any single section below.
| Command | What it answers |
|---|---|
atf status | Live board: services, listening ports and task counts in one read. |
atf validate --strict | Config, compose, catalog parity and a secrets scan; exits non-zero on warnings too. |
atf doctor | Deep checks across docker, the containers and their network aliases, ports, the API, the services, the database, logs, sessions, certificates, disk and AI providers. Exit code 0 when nothing failed, 1 when any check failed. |
atf logs query --level ERROR | Attributable error history — actor, service, subject task, correlation id. |
The same questions are answerable over HTTP, which is what a dashboard or a monitoring script uses:
curl -sf localhost:8080/healthz # is the gateway alive
curl -s localhost:8080/api/health/services | jq .status # healthy | degraded
curl -s localhost:8080/api/system/setup | jq '.ready, .missing_required'
curl -s localhost:8080/api/system/env | jq '.ok, .missing_required'| Probe | Reads |
|---|---|
GET /healthz | Liveness of the API process. Answers 200 while the gateway is up. |
GET /api/health/services | Live probe of every service. status is healthy only when all answer, otherwise degraded. |
GET /api/system/setup | ready folds the env contract and the service probes together; also returns missing_required and the version block. |
GET /api/system/env | The env contract itself: ok is true only when no required key is missing. |
A service will not start, or is unhealthy
Symptom. The dashboard is blank, forwarding stopped, or a service shows red on the board.Cause. One container is down — a crash loop, a missing env key, or a port already bound. The stack tolerates a down web or logger without stopping delivery, but a down core reads nothing from Telegram.
docker compose ps # state and ports of every container
atf docker ps # the same, ATF containers only
atf docker logs core # docker logs for atf-core (tail 100)
atf system services # state, health, endpoint and version; exit 1 if any is down
atf system services --json # {services:[...], healthy:N, total:4, failing:[...]}Confirm the fix with atf system services (all four OK) andcurl -s localhost:8080/api/health/services | jq .status returning healthy. The port contract each service is probed on is fixed:
| Service | Port | Role |
|---|---|---|
| api | 8080 | Go gateway — REST control plane for the dashboard, mobile and CLI. |
| web | 8088 | Rust Axum gateway plus the React SPA; also proxies /api. |
| core | 50051 | Python engine over gRPC — reads Telegram and runs the pipeline. |
| logger | 50052 | Log ingest and query service over gRPC. |
Dashboard login fails or loops
Symptom. The link from the bot bounces back to the login screen, or the dashboard keeps asking you to re-authenticate. Cause. A dashboard link carries a single-use code, not a session token: codes live 600 seconds, are burned on first exchange, and only the core that minted one can trade it for a session. A second tab, a refresh or a link replayed from chat history all fail the same way.
| Response | Meaning |
|---|---|
401 LOGIN_CODE_REQUIRED | No login code was sent to POST /api/auth/exchange-bot-token. |
401 INVALID_LOGIN_CODE | The code is used, expired or unknown. Core answers the same for all three on purpose. |
429 COOLDOWN | A still-valid link was minted for this user inside the 60-second cooldown. Retry-After carries the seconds left. |
# Exchange a bot-issued code for a 7-day session
curl -s -X POST localhost:8080/api/auth/exchange-bot-token \
-H 'Content-Type: application/json' \
-d '{"login_code":"<code from the bot>"}' | jq
# Ask for a fresh one-tap link (needs a Bearer token)
curl -si localhost:8080/api/auth/bot-login-link \
-H "Authorization: Bearer <token>" | grep -i retry-afteratf logs query --service core -s LOGIN-CODE -n 20
[LOGIN-CODE] refused reason=already_used user_id=1001
[LOGIN-CODE] refused reason=expired user_id=1001
[LOGIN-CODE] cooldown user_id=1001A task creates but never forwards
Symptom. The rule saves and shows as active, but posts from the source never reach the target.Cause. Almost always one of five things: the bot is not an admin in the source or the target, the channel id is a username where a numeric id is required (or the reverse), the rule is paused, deduplication recognises the post, or a filter drops it.
# Simulate a post through the live rule engine: no delivery is made
atf rules simulate <task-id-or-name> --text "the post text" --show
# The answer names matched / would_forward / reason / transformed_text,
# so a filter or a pause is visible instead of guessed.
atf rules list --json | jq '.[] | {id, name, enabled, paused}'| Log signal | What it tells you |
|---|---|
[PIPELINE:ROUTE] | A task matched the message. If you never see this, no rule matched — check source id and filters. |
[PIPELINE:FILTER_DROP] | A filter rejected the post. Expected only if the rule says so. |
[PIPELINE:CLEAN] | The cleaner removed content, leaving nothing worth sending. |
[PIPELINE:RESTRICTED_BYPASS] | Protected content was re-uploaded instead of forwarded. Normal copy-protection handling. |
[PIPELINE:DELIVERED] | Telegram confirmed the delivery. |
The bot must be an admin in both chats
A bot that is only a member cannot post. Telegram answers ChatAdminRequired, which ATF classifies as err_admin_required and surfaces as “check admin”. Add the bot as an administrator of the source and of each target, then re-run atf rules simulate.
Numeric id versus @username
Telegram resolves a numeric chat id and an @username differently, and a bot cannot always resolve a username it has never seen the chat through. When a target stays silent while the simulation matches, store the numeric id the bot reports and try again.
Paused rules keep their queued work
A paused rule never forwards new messages, and jobs already queued for it are deferred with backoff rather than dropped, so resuming continues exactly where it stopped. Check the rule state before assuming a bug.
Deduplication and filters
A SHA-256 content fingerprint suppresses a post the rule has already delivered, and any active filter — keyword, link rule, media type — drops it outright with a [PIPELINE:FILTER_DROP] line. The simulation reports the same decision without touching Telegram.
Messages stuck in the queue or dead-lettered
Symptom. Posts are accepted but nothing arrives, or the dashboard shows a growing backlog.Cause. A job is waiting in the durable queue or has exhausted its retries and landed in the dead-letter queue. Replay is deliberate: fix the cause first, then replay.
atf data queue # depth and in-flight jobs by state
atf data dlq # dead-lettered jobs with their errors
atf data dlq retry <id> # replay one job after the cause is fixed
atf data dlq purge # clear the DLQ once nothing is worth replayingcurl -s localhost:8080/api/queue -H "Authorization: Bearer ***" | jq
curl -s localhost:8080/api/dlq -H "Authorization: Bearer ***" | jq '.items'
curl -s -X POST localhost:8080/api/dlq/<id>/retry \
-H "Authorization: Bearer ***" | jq| State | Means |
|---|---|
PENDING | Queued, waiting for a worker. |
RUNNING | A worker holds the job. |
RETRY_WAIT | Waiting out its backoff — a paused task keeps jobs here by design. |
SUCCEEDED | Delivered. |
UNKNOWN | Telegram timed out; the outcome was never confirmed, so it is never reported as silent success or failure. |
DEAD_LETTER | Retries were exhausted — inspect the recorded error, fix the cause, then replay. |
FloodWait & protected posts
Symptom. Deliveries stop for a while, then resume on their own; or a post arrives re-uploaded instead of as a forward. Cause. Telegram rate-limits accounts (FloodWait), and channels can forbid forwarding. Neither is a bug.
- The account is marked time-limited for the wait Telegram named, and its deliveries pause.
- Jobs stay queued in
RETRY_WAIT; other accounts keep serving, so one limited account never stalls the installation. - When a forward is refused because the channel is protected, the post is re-uploaded instead. Repeated FloodWaits mean your rates are too high, not that the code is broken.
“Port already in use”
Symptom. A service fails to start with port already in use, often 8088or 8080. Cause. Another process — usually a previous stack that was not shut down — owns the port. The contract is fixed: api 8080, web 8088, core 50051, logger 50052.
atf validate --runtime # the ports check names the owning process
atf system restart # recreate the stack on the current .env
docker compose down # or tear the old stack down first
atf doctor # confirm 8080, 8088, 50051, 50052 all answering429 rate limiting
Symptom. The dashboard or a script gets 429 Too Many Requests. Cause.The API enforces per-route limits in front of the control plane, and the unauthenticated auth endpoints are limited hardest. Every refusal names itself, so a client can pace itself instead of discovering the limit by being refused.
curl -si localhost:8080/api/rules -H "Authorization: Bearer ***" | grep -i 'x-ratelimit\|retry-after'
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
Retry-After: 12
X-RateLimit-Degraded: 1 # limit enforced per process (Redis unreachable)| Status | Code | Reading it |
|---|---|---|
429 | rate_limited | Body carries retry_after_seconds, limit and the scope; the header carries Retry-After too. |
503 | rate_limit_unavailable | A security class could not evaluate its limit at all and fails closed — retry shortly, do not keep hammering. |
An AI provider is unreachable
Symptom. Rewritten posts arrive unmodified, or a rule with a rewrite configured stops sending.Cause. The provider is down, the key is wrong, or the rewrite was turned off for that task. A provider failure must never block delivery: the outcome is decided by the rule's fallback policy.
# Probe a provider key without sending anything
curl -s -X POST localhost:8080/api/ai-configs/check-key \
-H "Authorization: Bearer ***" -H 'Content-Type: application/json' \
-d '{"provider":"openai","model":"gpt-4o-mini"}' | jq '.success, .healthy, .error_code'
# Confirm the per-task toggle in the logs
atf logs query --level WARN -s AI_REWRITE -n 20# The pipeline asks one question: ai_rewrite_active.
# A task that has an ai_config_id rewrites, unless the task panel's
# paid-addon switch vetoes it. That switch is paid_addons.ai_rewrite.
atf rules settings set <task> paid_addons.ai_rewrite=false
atf rules simulate <task> --text "sample" --show| Fallback policy | Behaviour when the provider fails |
|---|---|
SEND_ORIGINAL | Deliver the untransformed text. Delivery is never blocked by the provider. |
DROP | Drop the post rather than deliver without the rewrite. The default policy. |
RETRY | Requeue the job and try the provider again later. |
QUARANTINE | Hold the job for review instead of guessing. |
Log history is empty or truncated
Symptom. Old entries are gone, or history is thinner than expected. Cause.History is bounded on purpose. The logger keeps a bounded in-memory queue and, when it is full, evicts theoldest queued record (reported as dropped) so the newest context survives. Age-based retention then deletes rows past the policy age — seven days by default — while audit and security categories are excluded from deletion by policy.
atf logs stats # volume by service and level
atf logs query --level ERROR --hours 24
atf logs purge --older-than-days 30 # a separate, audited action — not the same as shedding
curl -s localhost:8080/api/logs/policy -H "Authorization: Bearer ***" | jqA container is older than your .env
Symptom. The bot reports it is unconfigured although .env is filled, or the validator warns container-env. Cause. Environment variables are read when a container is created, so a container started before the last .env change keeps the old values.
atf validate --strict # container-env flags a container missing a key .env sets
atf system restart # recreate the containers on the current .env
atf doctor # confirm every service reconnected| Message | Fix |
|---|---|
container is missing KEY although .env sets it | atf system restart — recreate on the current environment. |
container-env warns /api/system/env unavailable | An older api image; atf system restart to pick up the current image. |
Collect evidence before you ask for help
A bug report is only actionable with the correlation id, the log slice and a machine-readable doctor report. Gather all three; they answer most follow-up questions before they are asked.
# 1. Correlation id — every request carries one end to end
atf logs query --level ERROR -n 20 # copy the correlation id
atf logs query --corr <correlation-id> # the whole story for that request
# 2. A log export (JSON or CSV)
atf logs export --format json -o atf-logs.json --hours 24
# 3. Machine-readable health
atf doctor --json # exit 0 clean, 1 if any check failed
atf validate --json # one JSON object on stdout
atf system services --json # {services:[...], healthy:N, total:4, failing:[...]}The command surface is documented in the CLI reference, the runtime runbooks in Operations & diagnostics, and what shipped in each version on the releases page. This page describes v2.2.4.