Technical reference for contributors and operators of the OpenCode Telegram Integration. End-user setup and bot commands live in README.md.
- Architecture
- Directory Structure
- Configuration
- API Reference
- Development
- Streaming and SSE Internals
- Runtime Behavior Notes
- Testing Notes
- Docker Details
- Contributing
┌─────────────────────────────────────────────────────────────────────┐
│ Telegram Supergroup (Forum) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Topic #1 │ │ Topic #2 │ │ Topic #3 │ ... │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼─────────────┼─────────────┼─────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ Integration Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ grammY Bot │ │TopicManager │ │StreamHandler│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ Instance Manager (Orchestrator) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Instance #1 │ │ Instance #2 │ │ Instance #3 │ ... │
│ │ Port 4100 │ │ Port 4101 │ │ Port 4102 │ │
│ │ opencode │ │ opencode │ │ opencode │ │
│ │ serve │ │ serve │ │ serve │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Key components:
- Integration layer (
src/integration.ts): wires everything together — creates the grammY bot, handles orchestrator events, owns OpenCode clients and SSE subscriptions, routes messages between Telegram and OpenCode. - Instance manager (
src/orchestrator/manager.ts): lifecycle of OpenCode instances — on-demand creation, health checks, crash recovery, idle timeout, SQLite-backed restart recovery. - Stream handler (
src/opencode/stream-handler.ts): bridges SSE events to Telegram — "Thinking..." progress messages, throttled text streaming, tool status, in-place message edits. - Topic manager (
src/forum/topic-manager.ts): topic → session mapping, automatic session creation for new topics, message routing.
src/
├── index.ts # Entry point
├── config.ts # Configuration from environment
├── integration.ts # Wires all components together
├── api-server.ts # External instance registration API
├── bot/
│ └── handlers/
│ └── forum.ts # Telegram message/command handlers
├── forum/
│ ├── topic-manager.ts # Topic → Session mapping logic
│ └── topic-store.ts # SQLite persistence for topic mappings
├── opencode/
│ ├── client.ts # OpenCode REST API client
│ ├── discovery.ts # Discover running OpenCode instances
│ ├── stream-handler.ts # SSE → Telegram message bridging
│ └── telegram-markdown.ts # Markdown conversion for Telegram
├── orchestrator/
│ ├── manager.ts # Manages multiple instances
│ ├── instance.ts # Single OpenCode instance lifecycle
│ ├── port-pool.ts # Port allocation
│ └── state-store.ts # SQLite persistence for instance state
└── types/
├── forum.ts # Forum/topic types
└── orchestrator.ts # Orchestrator types
Runtime state (gitignored) lives in data/:
data/orchestrator.db— instance statedata/topics.db— topic mappings
| Variable | Required | Default | Description |
|---|---|---|---|
TELEGRAM_BOT_TOKEN |
Yes | - | Bot token from @BotFather |
TELEGRAM_CHAT_ID |
Yes | - | Supergroup ID (starts with -100) |
TELEGRAM_ALLOWED_USERS |
No | (empty = all) | Comma-separated allowed user IDs |
HANDLE_GENERAL_TOPIC |
No | true |
Whether to handle General topic messages |
PROJECT_BASE_PATH |
No | ~/oc-bot |
Where topic directories are created |
AUTO_CREATE_PROJECT_DIRS |
No | true |
Auto-create project directories |
OPENCODE_PATH |
No | opencode |
Path to opencode binary |
OPENCODE_MAX_INSTANCES |
No | 10 |
Max concurrent instances |
OPENCODE_PORT_START |
No | 4100 |
Starting port for instances |
OPENCODE_PORT_POOL_SIZE |
No | 100 |
Number of ports in the pool |
OPENCODE_IDLE_TIMEOUT_MS |
No | 1800000 |
Idle timeout (30 min) |
OPENCODE_HEALTH_CHECK_INTERVAL_MS |
No | 30000 |
Health check interval (30 s) |
OPENCODE_STARTUP_TIMEOUT_MS |
No | 60000 |
Instance startup timeout (60 s) |
STREAM_UPDATE_INTERVAL_MS |
No | 1000 |
Min interval between progress-message edits in ms (clamped 500–5000) |
TELEGRAM_SEND_INTERVAL_MS |
No | 40 |
Bot-wide floor between Telegram API calls in ms, ≈25 msg/s (clamped 10–1000) |
PERMISSIONS_AUTO_ALLOW |
No | read,glob,grep,list,lsp |
Permission kinds auto-approved with "once" (CSV, case-insensitive; empty = approve nothing; unknown kinds always ask) |
ORCHESTRATOR_DB_PATH |
No | ./data/orchestrator.db |
Instance state database |
TOPIC_DB_PATH |
No | ./data/topics.db |
Topic mapping database |
API_PORT |
No | 4200 |
External API server port |
API_KEY |
No | - | Optional API key (recommended for production) |
See .env.example for all available options, including optional
webhook settings. Guided setup: bash scripts/setup-env.sh.
The bot exposes an API (default port 4200) for external OpenCode instances to register. Useful when running in Docker without process discovery, or for attaching long-lived instances started elsewhere.
# Register an external instance
curl -X POST http://localhost:4200/api/register \
-H "Content-Type: application/json" \
-d '{
"projectPath": "/path/to/project",
"projectName": "my-project",
"opencodePort": 4096,
"sessionId": "ses_abc123"
}'
# Unregister
curl -X POST http://localhost:4200/api/unregister \
-H "Content-Type: application/json" \
-d '{"projectPath": "/path/to/project"}'
# Check status
curl http://localhost:4200/api/status/$(echo -n "/path/to/project" | base64)
# List all instances
curl http://localhost:4200/api/instances
# Health check
curl http://localhost:4200/api/healthRegistering creates a Telegram forum topic for the project, subscribes to the session's SSE events, forwards responses to Telegram in real time, and routes Telegram replies back into the session.
Each OpenCode instance exposes:
GET /global/health # Health check
GET /session # List sessions
POST /session # Create session
GET /session/:id/message # Get messages
POST /session/:id/message # Send message (sync)
POST /session/:id/prompt_async # Send message (async)
GET /event # SSE event stream
# Install dependencies
bun install
# Start with hot reload
bun run dev
# Type check
bun run typecheck
# Format code (if prettier configured)
bun run formatProduction installs use scripts/install.sh, which is user-space-only: a
per-user systemd unit (systemctl --user), default prefix
~/.local/share/opencode-telegram, no root/sudo//opt. The operator flow
(flags, lingering, uninstall gating, migration off old system-wide
installs) is documented in
README.md; bash scripts/install.sh --help is authoritative for flags. Keep exactly one
service per bot token to avoid Telegram 409 Conflict errors.
- Event-driven: Orchestrator emits events, integration layer handles them
- State recovery: Both orchestrator and topic manager recover state on restart
- Graceful degradation: Errors are logged but don't crash the bot
- New bot commands: Add to
src/bot/handlers/forum.tsincreateForumCommands() - New SSE event handling: Modify
src/opencode/stream-handler.ts - New instance lifecycle events: Modify
src/orchestrator/instance.ts
Summary of how a prompt becomes streamed Telegram messages (see
.opencode/skills/silent-stop-debug/SKILL.md for the failure-mode map):
routeMessageToInstance(src/integration.ts) sends the prompt viaPOST /session/:id/prompt_async(fire-and-forget) and records the prompt per topic for restart recovery.- Each instance has one SSE subscription (
OpenCodeClient.subscribe/startSSEinsrc/opencode/client.ts) overGET /event, with bounded reconnect (5 retries, 1s→15s backoff, budget reset on every received event). If the stream is lost for good, the affected topic gets an honest notice instead of silence. StreamHandler.handleEvent(src/opencode/stream-handler.ts) routes events by session ID:message.part.updateddrives the "Thinking..." / streaming progress message,session.idlerenders the final answer,session.errordeletes progress and posts the error, andpermission.updatedposts an approve/deny card that blocks the session until answered.- Session IDs are extracted from several envelope shapes
(
props.sessionID,props.info,props.part,session.updatedinfo); events for unregistered sessions are dropped with a log line. - On crash/restart,
instance:readyre-subscribes and may create a new session; if the previous prompt had produced activity, the topic gets a "resend your message" nudge — accepted prompts are never silently dropped.
Concise operator/developer reference for recently shipped behavior. Source:
AgentsReport/builder/2026-09-19_*.md.
- Startup duplicate-bot guard. The bot takes a lockfile (
data/bot.lock) before polling; a second copy refuses to start with an error naming the live PID and lockfile path, exiting 1. A dead PID is taken over; only the owning process releases the lock. A Telegram409 Conflictat startup means two pollers on one token — kill the duplicate, don't restart yours. - Send pacing. All sends/edits flow through one shared queue: a bot-wide
floor (
TELEGRAM_SEND_INTERVAL_MS, default 40 ms) plus a per-message edit floor (STREAM_UPDATE_INTERVAL_MS). Final answers, cards, and notices are never dropped (wait + bounded retry, then caller fallbacks); only progress edits may be shed under pressure, and the trailing flush re-delivers the tail. Telegram429s back off queue-wide byretry-after+ cushion. - Burst coalescing. Each topic buffers inbound messages in a ~10 s sliding window: rapid messages merge into ONE prompt (newline-joined, arrival order, per-topic isolation). Every message waits out the window; while the session is busy the flush holds and retries every ~3 s, and pending text also flushes at turn-idle or on cancel (as the next turn). Retry taps bypass the window deliberately.
- Permission allowlist.
PERMISSIONS_AUTO_ALLOW(defaultread,glob,grep,list,lsp) auto-answers matching permission requests with"once"— loud server log plus a subtle in-chat note, turn continues, no card. Everything else (writes, exec, network,external_directory, unknown kinds) keeps the approve/deny card with its 5-minute reminder. Set-but-empty means approve nothing. Auto-allow API failure posts a loud resend notice, never silence. - Cancel. The Cancel button on progress cards and
/cancelin a topic abort the in-flight turn (abortSession), reset progress state, and post exactly one confirmation; the session stays registered and usable. Tapping with nothing in flight gets a polite no-op notice. Cancel never unregisters the session. - Interactive cards. Errors, crashes, SSE loss, and idle timeout post notices with Retry (resends the last prompt; busy/stale/expired taps are refused safely) and Restart/Reconnect (one-tap fresh session via the normal ready path; no-op if already running) buttons. A typing indicator heartbeats while a turn is active. Truncated finals get a Full output toggle (expand/collapse, bounded store); finals with tool activity add a threaded reply with per-tool lines and a tools/time/tokens footer. Progress cards cap at 8 tool lines with a "+N more" note.
- Topic retention (no auto-cleanup). Automatic stale-topic deletion was
removed: nothing deletes topics on its own, and the
STALE_TOPIC_*settings no longer exist (if set, they are silently ignored). Replacement: nothing automatic — use manual/clearin General (drops mappings whose sessions are gone) or/disconnectin a topic. Instance idle timeout is unchanged (it stops idle instances, not topics).
- Send messages in Telegram topics to exercise the full flow.
- Watch the terminal running
bun run devfor request, SSE, and error logs. - Inspect
data/orchestrator.dbanddata/topics.dbfor persisted state. - After a suspected silent failure, check
/statusin General (SSE subscription count, instance state), then send one more message in the stuck topic and note whether it recovers or errors loudly.
Native Bun is recommended for full functionality (especially session discovery). This section covers Docker for operators who need it.
| Feature | Native (Bun) | Docker | Docker + --pid=host |
|---|---|---|---|
/new - create managed instances |
Works | Works | Works |
/sessions - discover host sessions |
Works | No | Linux only |
/connect - attach to discovered sessions |
Works | No | Linux only |
| External API registration | Works | Works | Works |
| Stream responses to Telegram | Works | Works | Works |
- You only need managed instances (created via
/newcommand) - You're on Linux and can use
--pid=host - You want to use the External API to manually register instances
docker build -t opencode-telegram .docker run -d --name opencode-telegram \
--network=host \
-v $(pwd)/data:/app/data \
-v ~/oc-bot:/root/oc-bot \
--env-file .env \
opencode-telegramVolume Mounts:
| Mount | Purpose |
|---|---|
./data:/app/data |
SQLite databases for persistent state |
~/oc-bot:/root/oc-bot |
Project directories created by /new command |
docker run -d --name opencode-telegram \
--network=host \
--pid=host \
-v $(pwd)/data:/app/data \
-v ~/oc-bot:/root/oc-bot \
--env-file .env \
opencode-telegramWarning:
--pid=hostshares the host's process namespace with the container. The container can see all host processes.
Start the bot as in Option 1, then register host instances (see External Instance API).
On macOS and Windows, Docker Desktop runs containers in a Linux VM:
--network=hostdoesn't provide true host networking--pid=hostis not available- Discovery will not work - use native Bun or the External API
version: '3.8'
services:
opencode-telegram:
build: .
container_name: opencode-telegram
network_mode: host
# Uncomment for discovery (Linux only):
# pid: host
volumes:
- ./data:/app/data
- ~/oc-bot:/root/oc-bot
env_file:
- .env
restart: unless-stoppeddocker compose up -d
docker compose logs -f# View logs
docker logs -f opencode-telegram
# Check container status
docker ps -a --filter name=opencode-telegram
# Stop the bot
docker stop opencode-telegram
# Remove container
docker rm opencode-telegram
# Rebuild after code changes
docker build -t opencode-telegram . && docker compose up -dContributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.