Documentation menu

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.

Start here
CommandWhat it answers
atf statusLive board: services, listening ports and task counts in one read.
atf validate --strictConfig, compose, catalog parity and a secrets scan; exits non-zero on warnings too.
atf doctorDeep 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 ERRORAttributable 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:

The four probes
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'
API probes
ProbeReads
GET /healthzLiveness of the API process. Answers 200 while the gateway is up.
GET /api/health/servicesLive probe of every service. status is healthy only when all answer, otherwise degraded.
GET /api/system/setupready folds the env contract and the service probes together; also returns missing_required and the version block.
GET /api/system/envThe 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.

Isolate the failing container
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:

Port contract
ServicePortRole
api8080Go gateway — REST control plane for the dashboard, mobile and CLI.
web8088Rust Axum gateway plus the React SPA; also proxies /api.
core50051Python engine over gRPC — reads Telegram and runs the pipeline.
logger50052Log 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.

Login error codes
ResponseMeaning
401 LOGIN_CODE_REQUIREDNo login code was sent to POST /api/auth/exchange-bot-token.
401 INVALID_LOGIN_CODEThe code is used, expired or unknown. Core answers the same for all three on purpose.
429 COOLDOWNA still-valid link was minted for this user inside the 60-second cooldown. Retry-After carries the seconds left.
The auth endpoints
# 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-after
How it looks in the logs
atf 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=1001

A 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.

Dry-run the exact decision
# 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}'
Pipeline vocabulary
Log signalWhat 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.

Inspect and replay from the CLI
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 replaying
The same over HTTP
curl -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
Delivery job states
StateMeans
PENDINGQueued, waiting for a worker.
RUNNINGA worker holds the job.
RETRY_WAITWaiting out its backoff — a paused task keeps jobs here by design.
SUCCEEDEDDelivered.
UNKNOWNTelegram timed out; the outcome was never confirmed, so it is never reported as silent success or failure.
DEAD_LETTERRetries 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.

  1. The account is marked time-limited for the wait Telegram named, and its deliveries pause.
  2. Jobs stay queued in RETRY_WAIT; other accounts keep serving, so one limited account never stalls the installation.
  3. 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.

Find the owner, then reclaim the port
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 answering

429 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.

Read the quota off the response
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)
Rate-limit responses
StatusCodeReading it
429rate_limitedBody carries retry_after_seconds, limit and the scope; the header carries Retry-After too.
503rate_limit_unavailableA 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.

Prove the provider before blaming the pipeline
# 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
Turn the rewrite off, if that is the answer
# 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
AI fallback policy (per task)
Fallback policyBehaviour when the provider fails
SEND_ORIGINALDeliver the untransformed text. Delivery is never blocked by the provider.
DROPDrop the post rather than deliver without the rewrite. The default policy.
RETRYRequeue the job and try the provider again later.
QUARANTINEHold 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.

Check what retention and shedding did
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 ***" | jq

A 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.

Detect and fix
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
Container-env signals
MessageFix
container is missing KEY although .env sets itatf system restart — recreate on the current environment.
container-env warns /api/system/env unavailableAn 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.

One command each
# 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.