Agent skill
telegram
Operate the joelclaw Telegram channel — primary mobile interface between Joel and the gateway. Covers grammy Bot API, text/media/reactions, inline buttons, callbacks, and streaming.
Install this agent skill to your Project
npx add-skill https://github.com/joelhooks/joelclaw/tree/main/skills/telegram
SKILL.md
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: [email protected] — 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
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
skill-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.
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.
daily-summary
Generate a daily system activity summary across all joelclaw subsystems. Triggers on 'daily summary', 'what happened today', 'system report', 'today's tally', 'activity summary', 'what did we do', 'daily report'.
inngest-durable-functions
Create and configure Inngest durable functions. Covers triggers (events, cron, invoke), step execution and memoization, idempotency, cancellation, error handling, retries, logging, and observability.
recall
Fan-out search across all memory sources when context is unclear or vaguely referenced. Triggers on: 'from earlier', 'remember when', 'what we discussed', 'that thing with', 'the conversation about', 'did we ever', 'what happened with', 'you mentioned', 'we talked about', 'earlier today', 'last session', 'the other day', or any vague reference to past context that needs resolution before the agent can act.
inngest-local
Set up self-hosted Inngest on macOS as a durable background task manager for AI agents. Interactive Q&A to match intent — from Docker one-liner to full k8s deployment with persistent state. Use when: 'set up inngest', 'background tasks', 'durable workflows', 'self-host inngest', 'event-driven functions', 'cron jobs', or any request for a local workflow engine.
Didn't find tool you were looking for?