A Telegram ↔ jcode bridge built on the official @1jehuang/jcode-sdk. Messages from Telegram are injected into a local jcode daemon session and replies stream back to Telegram in real time.
Source: https://github.com/iasds/jcode-telegram-bridge
Telegram user ⇄ Bot API (native fetch long-poll, 15s poll)
⇅
bridge.ts: session mapping + command set + markdown rendering (hermes-agent port)
⇅ JcodeClient.connect() (unix socket)
jcode api-bridge ⇄ jcode daemon (shared live user session)
| Command | Purpose |
|---|---|
| /start /help | Welcome and help |
| /info | Session runtime info (provider / model) |
| /clear | Clear the current session history |
| /plan | Plan mode (plan only, no execution) |
| /model [name] | View / switch model |
| /compact | Request long-context compression |
| /cancel | Interrupt the current turn (verified: interrupts the daemon turn) |
| /status | Bridge and daemon status (bridge-only) |
- Pipeline:
voice/audio/video_note (.ogg/.opus)->getFile + fetch -> /tmp->tools/transcription_tools parity-> quoted"transcript"+captioninjected asenriched_text(mirrorsgateway/run.py:_enrich_message_with_transcription). - Defaults (per local approval):
STT_LANGUAGE=zh+STT_LOCAL_MODEL=small(small~500MB, better zh WER thanbaseon 2.2G box;tiny/base/small/medium/large-v3supported). - Provider order: explicit
STT_PROVIDERwins; autolocal (faster-whisper) > groq (whisper-large-v3-turbo) > openai (whisper-1)with fallback to local on cloud failure (Hermes_get_provider). - Limits:
25MB,SUPPORTED_FORMATS {.ogg/.opus/.mp3/.m4a/...}; oversize/too-large echoes[voice message too large]and still delivers caption;ffmpegtranscodes.ogg/.opus -> 16kHz mono 32k AAC m4afor cloud endpoints. - Echo:
STT_ECHO_TRANSCRIPTS=truesends🎙️ "transcript"before the agent turn (Hermes_echo_pending_stt_transcripts_once).
npm install && npm run build && npm test
# configure
cp .env.example .env # TELEGRAM_BOT_TOKEN / TELEGRAM_BOT_ALLOWED_IDS
# systemd user services (both)
systemctl --user enable --now jcode-api-bridge.service jcode-tg-bridge.service
sudo loginctl enable-linger "$USER" # auto-start on boot- Streaming replies (per-turn child connection): the bridge consumes
events()on a dedicated child connection per turn so replies stream into Telegram as progressive edits (throttled, with a ▉ cursor, tool segments, flood fallback). Falls back torun()only when the turn never started; a half-sent message is never re-run (double-execution guard). - Native fetch long-poll (not telegraf polling): telegraf 4.16's
getUpdates request hardcodes a 500s timeout, and its abort only covers the
request phase. When a transparent proxy sends response headers but stalls
the body,
res.json()hangs forever (poll dead, bridge deaf). We poll with native fetch +AbortSignal.timeout(45s)(covering the whole body read) + 15s short poll + exponential backoff + exit after 5 consecutive failures for systemd to restart. SeegetUpdatesRaw(). - Duplicate-reply protection: the poll offset is persisted to
poll-offset.txt(derived from thestateFiledirectory). A restart never re-pulls already-delivered updates — this is what broke the "bot loops sending the same message" bug (restart loop + offset=0 + re-handling the same update). The idempotency logic lives insrc/logic.tsand is unit tested. - Poisoned-session rotation: attaching a session that is stuck server-side makes the daemon reset the socket. On connection close, or on any attach failure, the mapping for the last-attached session is dropped and recreated, so a restarted process gets a fresh session instead of looping forever.
- Self-heal: SDK connection loss (
disconnected/connect_failed) or repeated poll failures exit with code 1; systemdRestart=alwaysbrings up a fresh process.getMeuses backoff (10 tries) so a transient proxy blip does not restart the bridge. - MarkdownV2 rendering ported from hermes-agent (NousResearch/hermes-agent,
open source): a 12-step pipeline (code-block protection, link conversion,
tables to bullets, escaping, safety net), see
src/markdown.ts, 14 unit tests. - Whitelist:
TELEGRAM_BOT_ALLOWED_IDScomma-separated; empty = allow all - Concurrency: one fixed jcode session per chat (JSON persisted), messages on the same session are queued serially; turns run in the background (fire-and-forget) so commands like /cancel stay responsive
- Full auto-approval: the bridge has no permissions capability; tools run automatically (only reachable by whitelisted users)
npm test # node --test — full suite (see test/; currently 138 cases)src/bridge.tsentry / bot layer / native fetch poll / routing / self-healsrc/commands.tscommand setsrc/logic.tspure reliability helpers (offset idempotency, rotation decision)src/markdown.tsMarkdownV2 rendering (hermes-agent port)src/events.tsrendering glue (working line, tool lines, final reply)src/stream.tsstreaming reply renderer (progressive edits)src/model-picker.tsinteractive /model selectorsrc/sessions.tschat→session mapping + concurrency queue + persistencesrc/config.tsenvironment configurationtest/*.test.mjsunit tests~/.config/systemd/user/jcode-{api-bridge,tg-bridge}.servicesystemd units
- Loop diagnosis: a restart loop (repeated
fatal connection errorlines, growingNRestarts) usually means the daemon reset the socket because an attached session is poisoned. Check forrotating sessionwarnings, then send any message to rebuild the session. The persistedpoll-offset.txtguarantees a restart never re-sends old replies. - Stream stages:
[stream] connect → connected → attach → attached → consuming events → turn_done (N events) → loop end → finished. If it stops beforeattached, the session is likely broken (rotation kicks in). /statusincludes an STT health line (STT worker: resident (fast) | down, inline fallback | idle, withdeaths:N respawns:Mafter crashes) and poll failures escalate: >=10getUpdateserrors within one hour log aNETWORK HEALTHwarning once per hour (proxy-chain degradation signal).- STT: each voice logs
[stt] voice ok|fail provider=… dur=…ms. A resident Python worker (src/stt_worker.py) loadsfaster-whisper small (zh)once at boot ([stt_worker] model 'small' loaded …/[stt] resident worker warm in ~3s), so per-voice latency is warm-end (~3s) instead of a cold model load (~8s). If the worker dies it is respawned on demand; if respawn fails, transcription falls back to the in-process loader for that request. Concurrent voices are capped at 2. Failed transcripts still leave the durable.jcode-media/telegram-voice/*.oggfor the agent and log the anchor path. Pruning runs at boot (keep 7d / 500MB). - Silent bridge: watch journald for
getUpdates error(rate-limited: attempt 1, every 10th, and the fatal 5th are logged; arecovered after N failed attempts (outage …)summary marks recovery),429retries, andNRestarts(systemctl --user show jcode-tg-bridge.service -p NRestarts). - Config hardening:
TURN_TIMEOUT_MS(10s–30min) andQUEUE_LIMIT(1–20) are clamped; unknownSTT_LOCAL_MODELfalls back tosmallwith a warning. - Commands vs TUI: /status and /info are bridge-only; all others align with the jcode TUI. Unknown commands are rejected, never sent to the agent.
- The proxy (Clash) can still stall long-poll bodies; the 45s whole-read
timeout mitigates it. If the bridge goes deaf, watch journald
getUpdates errorandNRestarts. - Photos are not supported (no vision capability in the current model).
Text documents ARE handled inline (text/* or known text extensions up to
MAX_INLINE_DOC_BYTES, content injected as[Content of <name>]:); other files are announced as attachments. Voice/audio/video_note are fully supported via the STT pipeline above.