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

ComponentLocationPurpose
N8N workflowBMS4 N8N instance (telegram-claude-bot, id c0cRdAA8vJc9wjIE)Telegram webhook → command routing → HTTP call to claude-session-manager
claude-session-managerBMS4, systemd service, port 9997, as claude-runnerOwns both the /ask one-shot-subprocess path and the legacy tmux path
Dedicated Claude workdir/home/claude-runner/telegram-workdir/p24-infracwd 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-runnerPersistent Claude CLI session used only by /start /send /log /stop /restart
setup.shinfra-src/telegram-claude-bot/setup.shOne-shot BMS4 configuration
sudoers snippet/etc/sudoers.d/n8n-claude-runnerAllows N8N to run commands as claude-runner (legacy path)

Bot Commands Reference

CommandMechanismExpected output
/ask <question> or plain messageone-shot claude -p subprocess, cwd = dedicated workdir, JSON-stitched history, up to 300sClaude’s answer (n8n node timeout 310000ms)
/conversationslist named conversations for this chatnumbered 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-historyclear history for the active conversation”History cleared for conversation: X”
/end-conversation-remove-historyarchive + remove active conversation, switch to main”Conversation ‘X’ archived. Switched to main.”
/contextmessage count + context-window usage”Messages: N / In context window: M/60”
/status (legacy)tmux list-sessions + ps aux grep claude + free -hSession 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 -pClaude output (last 30 lines)
/log (legacy)tmux capture-pane -p -S -50Last 50 lines of session

Security Model

  • User allowlist: N8N workflow checks message.from.id against AUTHORIZED_TELEGRAM_ID env 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/bash is 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) — see server.py’s WORK_DIR constant (#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:

  1. SSH to BMS4: ssh ubuntu@54.36.123.110
  2. Check N8N: docker compose -f /opt/n8n/docker-compose.yml ps
  3. 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:

  1. Edit infra-src/telegram-claude-bot/claude-session-manager/server.py, merge PR to main
  2. 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)
  3. scp to bms-4, sudo cp into /opt/claude-session-manager/server.py, sudo systemctl restart claude-session-manager
  4. Verify: curl http://localhost:9997/healthok, and systemctl show claude-session-manager -p MainPID,ActiveEnterTimestamp shows 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 returned nodes array, PUT /api/v1/workflows/{id} with only {name, nodes, connections, settings}. Auth: X-N8N-API-KEY header, key = BMS4_N8N_API_KEY in secrets/n8n-bms4.env.sops. Workflow id: c0cRdAA8vJc9wjIE. See /n8n skill for the full pattern. Always sync the repo’s n8n-workflow.json afterward (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):
    1. Edit infra-src/telegram-claude-bot/n8n-workflow.json, merge PR to main
    2. Open N8N (https://n8n.bms-4.infra.zintegrowana.online), open workflow telegram-claude-bot
    3. Workflows → Import from file → overwrite existing
    4. Re-select credentials in both Telegram nodes (import does not carry credential bindings)
    5. Re-activate workflow

Incident Response

After any server.py change lands on main: the file has no CI auto-deploy. Follow “Deployment / Update Procedure” above to scp it to bms-4 and restart claude-session-manager, or curl http://localhost:9997/health will keep hitting the old code.

Bot not responding

  1. Check N8N workflow is Active
  2. docker compose logs n8n --tail=100 on BMS4
  3. Restart N8N if needed: docker compose restart n8n
  4. 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 /log path)
  • Just send /start to 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.

  • 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