Telegram Claude Bot — Operations Guide
Service: telegram-claude-bot
Host: bms-4 (54.36.123.110)
Type: N8N workflow + tmux session manager
Status: Active
Implements: GitHub issue #608
Architecture
The bot has two execution paths sharing one HTTP backend (claude-session-manager, port 9997):
Live path — /ask (plain messages and explicit /ask <question>):
[Telegram user: radieu]
|
| any message, or /ask <question>
v
[Telegram Bot API] ──webhook (HTTPS)──► [N8N BMS4]
Telegram Trigger → Check authorized user (AUTHORIZED_TELEGRAM_ID)
→ Route Command → "Exec: ask" httpRequest node (timeout 310000ms)
|
v
[claude-session-manager :9997, claude-runner, systemd]
ThreadingHTTPServer — each request runs in its own thread (#4002).
1. loads /home/claude-runner/telegram-worker/sessions/<chat_id>/<conv>.history.json
2. stitches last 60 exchanges into the prompt
3. background thread: Telegram sendChatAction=typing every ~4s
4. run_pgroup("claude --dangerously-skip-permissions -p ...",
cwd=/home/claude-runner/telegram-workdir/p24-infra, timeout=300s)
— Popen(start_new_session=True); on TimeoutExpired: killpg SIGTERM,
grace, then SIGKILL so no `claude` grandchild leaks to PID 1 (#4002)
5. under per-chat lock: appends the exchange back to the JSON history file
so concurrent messages to the same chat can't corrupt/lose history (#4002)
|
v
n8n receives the HTTP response → Send Telegram Reply
v
[Telegram user]
No persistent Claude session and no --resume — “memory” is entirely the JSON-stitched history
(60-exchange window, warns at 80 messages). The claude process is a brand-new one-shot subprocess
per message, exiting once it answers. The n8n node timeout (310000ms) must stay ≥ the subprocess
timeout (300s) — a shorter n8n timeout silently drops replies (see Incident Response below).
Legacy path — /start /stop /restart /send /log (issue #608’s original design):
[Telegram user] → /start /stop /restart /send /log → [N8N] → Exec:<cmd> httpRequest node
→ claude-session-manager → tmux session "claude-agent" (persistent) → capture-pane output → reply
Still present and functional in server.py, but not what live traffic uses today — plain chat
and /ask bypass tmux entirely.
Components
| Component | Location | Purpose |
|---|---|---|
| N8N workflow | BMS4 N8N instance (telegram-claude-bot, id c0cRdAA8vJc9wjIE) | Telegram webhook → command routing → HTTP call to claude-session-manager |
| claude-session-manager | BMS4, systemd service, port 9997, as claude-runner | Owns both the /ask one-shot-subprocess path and the legacy tmux path |
| Dedicated Claude workdir | /home/claude-runner/telegram-workdir/p24-infra | cwd for every /ask subprocess — isolated from the queue-worker checkout at /opt/p24-infra (#3708) |
| Per-chat history | /home/claude-runner/telegram-worker/sessions/<chat_id>/ | JSON conversation history + index, keyed by Telegram chat ID and named conversation |
| tmux sessions (legacy) | BMS4, as claude-runner | Persistent Claude CLI session used only by /start /send /log /stop /restart |
| setup.sh | infra-src/telegram-claude-bot/setup.sh | One-shot BMS4 configuration |
| sudoers snippet | /etc/sudoers.d/n8n-claude-runner | Allows N8N to run commands as claude-runner (legacy path) |
Bot Commands Reference
| Command | Mechanism | Expected output |
|---|---|---|
/ask <question> or plain message | one-shot claude -p subprocess, cwd = dedicated workdir, JSON-stitched history, up to 300s | Claude’s answer (n8n node timeout 310000ms) |
/conversations | list named conversations for this chat | numbered list, active one marked |
/conversation <name|number> | switch active conversation | ”Switched to conversation: X” |
/new-conversation [name] | start a new, empty conversation | ”New conversation started: X” |
/new-conversation-reset-history | clear history for the active conversation | ”History cleared for conversation: X” |
/end-conversation-remove-history | archive + remove active conversation, switch to main | ”Conversation ‘X’ archived. Switched to main.” |
/context | message count + context-window usage | ”Messages: N / In context window: M/60” |
/status (legacy) | tmux list-sessions + ps aux grep claude + free -h | Session list, PID, CPU/RAM |
/start (legacy) | tmux new-session -d -s claude-agent -x 220 -y 50 | ”Session claude-agent started” |
/stop (legacy) | tmux kill-session -t claude-agent | ”Session claude-agent stopped” |
/restart (legacy) | kill + sleep 1 + start | ”Session claude-agent restarted” |
/send <prompt> (legacy) | tmux send-keys … Enter; sleep 5; tmux capture-pane -p | Claude output (last 30 lines) |
/log (legacy) | tmux capture-pane -p -S -50 | Last 50 lines of session |
Security Model
- User allowlist: N8N workflow checks
message.from.idagainstAUTHORIZED_TELEGRAM_IDenv var - No response to unauthorized: messages from other users are silently dropped (NoOp node)
- Credential storage: Telegram bot token lives in N8N credentials store only — never in git
- Process isolation: commands run as
claude-runner(non-root) via sudo - Sudoers scope: only
/usr/bin/bashis permitted, not all commands - Workspace isolation: Claude’s working directory is a dedicated clone at
/home/claude-runner/telegram-workdir/p24-infra, separate from/opt/p24-infra(the checkout shared by the autonomous queue-worker fleet) — seeserver.py’sWORK_DIRconstant (#3708)
EU AI Act — Risk Classification
- Risk tier:
limited(Art. 52 — transparency obligation only) - Rationale: This is a remote control interface for human-initiated Claude sessions. No autonomous decision-making, no scoring of persons, no automated allocation. The human (radieu) sends every prompt explicitly.
- Transparency: The bot is operated by the owner for personal infrastructure management.
Daily Operations
Check bot is responding
Send /status from Telegram. If no response within 30 seconds:
- SSH to BMS4:
ssh ubuntu@54.36.123.110 - Check N8N:
docker compose -f /opt/n8n/docker-compose.yml ps - Check workflow is Active in N8N UI
Start a Claude session
/start
Then send prompts with /send <your prompt>.
View live session output
/log
Note: /send waits 5 seconds for output. For long-running tasks, use /log after a delay.
Kill and restart session
/restart
Deployment / Update Procedure
server.py (claude-session-manager) — no CI auto-deploy, manual SSH step:
- Edit
infra-src/telegram-claude-bot/claude-session-manager/server.py, merge PR tomain - On Windows, extract the merged file with
[System.IO.File]::WriteAllText(...)(NOT PowerShell>/Out-File— those write UTF-16 with BOM and silently corrupt the Python source; this broke the #3708 deploy on first attempt) scpto bms-4,sudo cpinto/opt/claude-session-manager/server.py,sudo systemctl restart claude-session-manager- Verify:
curl http://localhost:9997/health→ok, andsystemctl show claude-session-manager -p MainPID,ActiveEnterTimestampshows a fresh PID/timestamp
n8n-workflow.json — two ways to apply, prefer the API for single-field changes:
- Fast path (single node field, e.g. a timeout): n8n REST API —
GET /api/v1/workflows/{id}, patch the specific node in the returnednodesarray,PUT /api/v1/workflows/{id}with only{name, nodes, connections, settings}. Auth:X-N8N-API-KEYheader, key =BMS4_N8N_API_KEYinsecrets/n8n-bms4.env.sops. Workflow id:c0cRdAA8vJc9wjIE. See/n8nskill for the full pattern. Always sync the repo’sn8n-workflow.jsonafterward (separate PR is fine) — the next manual re-import below would otherwise silently regress an API-only change. - Full re-import (structural changes, new nodes):
- Edit
infra-src/telegram-claude-bot/n8n-workflow.json, merge PR tomain - Open N8N (
https://n8n.bms-4.infra.zintegrowana.online), open workflowtelegram-claude-bot - Workflows → Import from file → overwrite existing
- Re-select credentials in both Telegram nodes (import does not carry credential bindings)
- Re-activate workflow
- Edit
Incident Response
After any
server.pychange lands onmain: the file has no CI auto-deploy. Follow “Deployment / Update Procedure” above to scp it to bms-4 and restartclaude-session-manager, orcurl http://localhost:9997/healthwill keep hitting the old code.
Bot not responding
- Check N8N workflow is Active
docker compose logs n8n --tail=100on BMS4- Restart N8N if needed:
docker compose restart n8n - Verify Telegram webhook: N8N Telegram Trigger node → Test Step
tmux session disappeared
- Sessions are lost on server reboot or manual kill (only affects the legacy
/start/send/logpath) - Just send
/startto recreate
Claude process hung (legacy tmux path)
/stop
/start
Bot shows “typing…” forever, never replies (/ask path)
Confirmed root cause (2026-07-11): journalctl -u claude-session-manager shows BrokenPipeError: [Errno 32] Broken pipe when the server tries to write the response. This means Claude did
finish and produce an answer, but n8n’s “Exec: ask” httpRequest node gave up and closed the
connection first because its options.timeout was shorter than the subprocess’s own 300s timeout
(150000ms vs 300000ms — n8n’s typing-indicator pings keep going via a separate direct call to the
Telegram API, so “typing…” shows even though the underlying request already died).
Fix: keep n8n’s “Exec: ask” node timeout ≥ the server.py subprocess timeout (currently
310000ms, matching the “Exec: wait” node). Verify: journalctl -u claude-session-manager --since "10 min ago" | grep BrokenPipe — any hits mean the mismatch is back.
Bot goes silent for minutes at a time (fixed 2026-07-12, #4002)
Historical root cause: server.py was single-threaded (HTTPServer(...).serve_forever()). One
slow /ask (up to 300 s) blocked EVERY other request, including /health. Users saw the bot as
“dead” until the current subprocess exited. Verify the fix is deployed:
curl -w '%{time_total}\n' -o /dev/null -s http://localhost:9997/health returns in <100 ms even
while an /ask is in-flight, and python3 -c "import ast; open('/opt/claude-session-manager/server.py').read()"
shows ThreadingHTTPServer in the file.
Zombie claude processes at PID 1 after timeout (fixed 2026-07-12, #4002)
Historical root cause: subprocess.run(..., timeout=300) killed only the sh -c wrapper, leaving
the claude grandchild to reparent to init and burn ~274 MB RSS + Anthropic tokens per orphan.
server.py now uses run_pgroup(...) which launches the shell in a new process group
(start_new_session=True) and os.killpg on timeout. Verify: after a /ask timeout, run
ps -eo ppid,comm | awk '$2=="claude" && $1==1' — output must be empty.
Related
- Setup guide:
infra-src/telegram-claude-bot/README.md - N8N workflow:
infra-src/telegram-claude-bot/n8n-workflow.json - Setup script:
infra-src/telegram-claude-bot/setup.sh - BMS4 operations:
docs/servers/p4-ovh-bms-4-ns3101999-operations.md - GitHub issue: https://github.com/radieu/p24-infra/issues/608