telegram
Telegram Channel Skill
Operate the joelclaw Telegram channel — the primary mobile interface between Joel and the gateway agent. Built on grammy (Bot API wrapper), supports text, media, reactions, replies, inline buttons, callbacks, and streaming.
Architecture
Joel (Telegram app)
→ Bot API (long polling via grammy)
→ telegram.ts channel adapter
→ enrichPromptWithVaultContext()
→ command-queue → pi session
→ outbound router → telegram.ts send → Bot API → Joel
Key files:
packages/gateway/src/channels/telegram.ts— channel adapter (inbound + outbound)packages/gateway/src/telegram-stream.ts— streaming UX (progressive text updates)packages/gateway/src/outbound/router.ts— response routingpackages/gateway/src/channels/types.ts—Channelinterface
SDK: grammy@1.40.0 — Bot instance at module scope, exposed via getBot().
Multi-instance poll ownership (2026-03-05): Telegram long polling now uses a Redis lease per bot token hash.
- Owner key:
joelclaw:gateway:telegram:poll-owner:<tokenHash> - Status key:
joelclaw:gateway:telegram:poll-status:<tokenHash> - Only owner polls
getUpdates; non-owners stay passive/send-only and retry lease acquisition with backoff.
Conflict guard still applies for non-cooperative pollers: telegram.channel.start_failed (with conflict metadata) + telegram.channel.retry_scheduled + telegram.channel.polling_recovered.
Capabilities
Sending Messages
// Via channel adapter
await telegramChannel.send("telegram:7718912466", "Hello", { format: "html" });
// Direct grammy API (from telegram-stream or daemon)
const bot = getBot();
await bot.api.sendMessage(chatId, text, { parse_mode: "HTML" });
- Max message length: 4096 chars (Telegram API limit)
- Chunking:
TelegramConverter.chunk()for HTML-aware splitting,chunkMessage()for raw text - Format: markdown→HTML via
TelegramConverter.convert(), with plain text fallback on validation failure - Buttons:
InlineButton[][]→inline_keyboardreply markup
Reactions (ADR-0162)
// grammy API
await bot.api.setMessageReaction(chatId, messageId, [
{ type: "emoji", emoji: "👍" }
]);
Telegram supports a fixed set of emoji reactions. Common ones: 👍 👎 ❤️ 🔥 🎉 🤔 👀 ✅ ❌ 🤯 💯
Agent convention: Include <<react:EMOJI>> at the start of a response. The outbound router strips it and calls setMessageReaction before sending text.
Replies
// grammy API — reply to a specific message
await bot.api.sendMessage(chatId, text, {
reply_parameters: { message_id: targetMessageId }
});
Already wired in the adapter via RichSendOptions.replyTo. The agent uses <<reply:MSG_ID>> directive.
Media
Supports photo, video, audio, voice, and document sending/receiving:
// Send
await telegramChannel.sendMedia(chatId, "/path/to/file.jpg", { caption: "Look at this" });
// Receive — handled by bot.on("message:photo") etc.
// Downloads via Bot API getFile → local /tmp/joelclaw-media/
// Emits media/received Inngest event for pipeline processing
File size limit: 20MB download via Bot API (larger files need direct Telegram API).
Streaming (ADR-0160)
Progressive text updates with cursor:
import { begin, pushDelta, finish, abort } from "./telegram-stream";
// On prompt dispatch
begin({ chatId, bot, replyTo });
// On each text_delta event
pushDelta(delta);
// On message_end
await finish(fullText);
- Plain text during streaming (no parse_mode) — avoids broken HTML on partial content
- HTML formatting only on
finish()— final edit withparse_mode: "HTML" - Throttled edits: 800ms minimum between API calls
- Cursor:
▌appended during streaming, removed on finish initialSendPromiseawaited infinish()to prevent race conditions
Inline Buttons & Callbacks (ADR-0070)
// Send message with buttons
await sendTelegramMessage(chatId, "Choose:", {
buttons: [
[{ text: "✅ Approve", action: "approve:item123" }],
[{ text: "❌ Reject", action: "reject:item123" }],
]
});
// Callback handler fires telegram/callback.received Inngest event
// Then edits message to show action taken + removes buttons
Callback data max: 64 bytes. Format: action:context.
Commands
/stop— abort current turn without killing the daemon./esc— alias for/stop./kill— hard stop: disables launchd service + kills process. Emergency use only.
Configuration
Currently via environment variables (migrating to ~/.joelclaw/channels.toml per ADR-0162):
| Env Var | Purpose |
|---|---|
TELEGRAM_BOT_TOKEN |
Grammy bot token |
TELEGRAM_USER_ID |
Joel's Telegram user ID (only authorized user) |
Security
- Single-user lockdown — middleware drops all messages from users other than
TELEGRAM_USER_ID - No token in config —
channels.tomlreferencesagent-secretskey names, not raw tokens - Media downloads to
/tmp/joelclaw-media/with UUID filenames (no path traversal)
Troubleshooting
Bot not receiving messages
- Check gateway is running:
cat /tmp/joelclaw/gateway.pid && ps aux | grep daemon.ts - Check Telegram polling started:
grep "telegram.*started" /tmp/joelclaw/gateway.log - Verify token:
curl https://api.telegram.org/bot<TOKEN>/getMe - Check polling errors in stderr:
rg "telegram.channel.start_failed|failed to start polling|getUpdates" /tmp/joelclaw/gateway.err - Check ownership lifecycle telemetry:
joelclaw otel search "telegram.channel.poll_owner" --hours 1joelclaw otel search "telegram.channel.retry_scheduled" --hours 1
If you see repeated 409 conflicts, another bot process is polling the same token. Telegram phone/desktop apps are not Bot API pollers and do not cause getUpdates contention.
Messages arriving but no response
- Check command queue:
grep "command-queue\|enqueue" /tmp/joelclaw/gateway.log | tail -10 - Check pi session health:
grep "session\|prompt" /tmp/joelclaw/gateway.log | tail -10 - Check outbound routing:
grep "outbound\|response ready" /tmp/joelclaw/gateway.log | tail -10
Streaming not working
- Verify
text_deltaevents:grep "text_delta" /tmp/joelclaw/gateway.log | tail -5 - Check
telegram-streamlifecycle:grep "telegram-stream" /tmp/joelclaw/gateway.log | tail -10 - Common issue: model does tool calls before text → no deltas until after tools complete
- Race condition fix:
initialSendPromiseinfinish()(commit 175c6ca)
HTML formatting broken
- Check converter output:
TelegramConverter.convert(text)+.validate(result) - Fallback: adapter auto-strips HTML and sends plain text if validation fails
- Streaming path sends plain text (no parse_mode), only
finish()adds HTML
Related ADRs
- ADR-0042 — Media download pipeline
- ADR-0070 — Inline buttons and callbacks
- ADR-0160 — Telegram streaming UX
- ADR-0162 — Reactions, replies, and channel configuration
More from joelhooks/joelclaw
cli-design
Design and build agent-first CLIs with HATEOAS JSON responses, context-protecting output, and self-documenting command trees. Use when creating new CLI tools, adding commands to existing CLIs (joelclaw, slog), or reviewing CLI design for agent-friendliness. Triggers on 'build a CLI', 'add a command', 'CLI design', 'agent-friendly output', or any task involving command-line tool creation.
129k8s
>-
88docker-sandbox
Create, manage, and execute agent tools (claude, codex) inside Docker sandboxes for isolated code execution. Use when running agent loops, spawning tool subprocesses, or any task requiring process isolation. Triggers on "sandbox", "isolated execution", "docker sandbox", "safe agent execution", or when working on agent loop infrastructure.
86joel-writing-style
Joel's writing voice and style guide for joelclaw.com content. Use when writing, editing, or reviewing any blog post, essay, book chapter, or prose content for joelclaw.com. Also use when asked to 'write like Joel,' 'match Joel's voice,' 'draft a post,' 'write content for the blog,' or 'review this for voice.' This skill captures Joel's specific writing patterns derived from ~90,000 words of published content spanning 2012–2026. Cross-reference with copy-editing and copywriting skills for marketing-specific copy.
81task-management
Manage Joel's task system in Todoist. Triggers on: 'add a task', 'create a todo', 'what's on my list', 'today's tasks', 'what do I need to do', 'remind me to', 'inbox', 'complete', 'mark done', 'weekly review', 'groom tasks', 'what's next', or when actionable items emerge from other work. Also triggers when Joel mentions something he needs to do in passing — capture it.
54skill-review
Audit and maintain the joelclaw skill inventory. Use when checking skill health, fixing broken symlinks, finding stale skills, or running the skill garden. Triggers: 'skill audit', 'check skills', 'stale skills', 'skill health', 'skill garden', 'broken skill', 'skill review', 'fix skills', 'garden skills', or any task involving skill inventory maintenance.
49