dsh-telegram

August 24, 2026 ยท View on GitHub

Recreate the web-app control experience for DeepSeek Harness agents on Telegram.
๐Ÿค– Chat with dsh agents from a phone ยท ๐Ÿ—‚๏ธ Drive sessions/models/presets/workspaces with buttons ยท ๐Ÿ”ง Live status & queue counts ยท ๐Ÿ›ก๏ธ Multi-chat isolation and fail-closed routing

English | ็ฎ€ไฝ“ไธญๆ–‡

Node Version License Tests dsh

Why โ€ข Features โ€ข Quick Start โ€ข Configuration โ€ข Architecture โ€ข Commands โ€ข How It Works โ€ข Tests โ€ข Docs โ€ข Safety


Why?

DeepSeek Harness's web UI is the gold standard for controlling an agent: sessions, models, workspaces, goals, presets, approvals. This plugin brings that surface to Telegram with the messaging ergonomics humans actually expect from a phone bot:

DimensionTypical Telegram botdsh-telegram
๐Ÿค– ChatOne global session, implicit stateOne agent per chat, bound sessions, fail-closed routing
๐Ÿงญ NavigationLong slash-command listsPersistent reply-keyboard bar + paginated inline cards
๐Ÿงฉ ModelsText config or guessworkProvider cards, 12/page paging, per-session reasoning picker
๐Ÿ“ฌ QueueInvisible inboxLive โŒ› Queue ยท N count embedded in the bar key
๐Ÿ›ก๏ธ ApprovalNo path on mobileInline Allow/Reject cards settled in place (buttons removed)
๐Ÿ“Ž AttachmentsDropped silentlyPhotos enter the session; documents/voice/video get clear guidance
๐Ÿ’ฌ RepliesPlain messagesNative reply-quote to the triggering message, clean reply surface
๐Ÿ“ˆ StreamingEverything at onceHarness-native live feed: Think lane, presenter-driven tool rows, and a typewriter answer that streams as it generates

Features

  • ๐Ÿ”€ Multi-chat isolation โ€” per-chat agent bindings, per-chat FIFO inbound router that spans the whole createโ†’bindโ†’deliver path (two rapid first messages can never create two sessions), unbound chats fail closed for display too
  • โšก Responsive UI lane โ€” bar buttons and inline callbacks now also ride a dedicated control:<chat> outbound queue, so Goal/Todos/Queue/ๆ”ถ่ตท react instantly while assistant streaming occupies the content queue
  • ๐Ÿ“ˆ Context-pressure compaction (#8) โ€” compact.threshold/compact.policy/compact.cooldownMs trigger an approval card or auto compaction near the model window, then announce the summary and shadowed tokens
  • ๐Ÿ“Ž Media (#9) โ€” multi-photo media groups become one user turn, voice transcribes via media.transcribe.*, documents/videos land in the session attachments directory
  • ๐Ÿ“‹ Todos (#10) โ€” /todo, a ๐Ÿ“‹ Todos ยท N bar entry, and incremental todo cards from durable todo/write events
  • โšก Responsive UI lane โ€” bar buttons and inline callbacks (collapse bar, Goal, menu navigation, question cards) run immediately instead of waiting behind a slow inbound turn; session creation stays serialized per chat across lanes
  • ๐ŸŽ›๏ธ Button-first UX โ€” persistent reply bar (โ˜ฐ Menu ยท โœจ New ยท ๐Ÿงฉ Models โ€ฆ) plus ephemeral inline cards for sessions, workspaces, goals, skills, subagents, presets, settings, credentials, llm/models, host, jobs, plugins and dynamic inventory
  • ๐Ÿ—‚๏ธ Project-grouped sessions โ€” the Sessions card mirrors the web display-title chain (session/title โ†’ cwd basename โ†’ id), groups sessions by workspace project, opens the running project first, offers a ๐Ÿ”€ ้กน็›ฎ switcher, per-row Chinese ๅฝ’ๆกฃ/ๅˆ ้™ค actions, and hides archived sessions with a live ๐Ÿ—„N count
  • ๐Ÿ’ก Bar control โ€” Menu page 1 has a ๐Ÿ’ก ๆ”ถ่ตท Bar / ๆ˜พ็คบ Bar switch and /bar [on|off] toggles the keyboard; the bar's ๐Ÿ—œ๏ธ ๆ”ถ่ตท hides it without leaving any carrier message behind
  • ๐ŸŽฏ Goal in menu โ€” Goal lives in the first menu page (shares a row with Capabilities); the card is display/edit/pause/๐Ÿ—‘ Clear goal (or /goalclear) only and never disturbs the running session; /goal <objective> [maxRounds] starts a goal, long goal turns get a step/tool progress card that collapses into the compact receipt with the cache hit rate
  • ๐Ÿ—‚๏ธ Workspaces are usable โ€” a Workspace detail card has โœ… ไฝฟ็”จๆญค้กน็›ฎ (set active project for new sessions) and ๐Ÿงญ ไผš่ฏ (open its sessions)
  • ๐ŸŒ Web-parity surface โ€” adapters mirror the web ApiProxy RPC contract: session.list/search/create/history/models/selectModel/prompt/attachment/updateQueue/cancel, subagents, host, workspace, agent presets, skills, goals, settings, credentials, llm providers/discovery
  • โšก Harness-native live feed โ€” reuses DeepSeek Harness's own output pipeline end to end: the same session/event stream, web-chat assistant block folding (assistant/chunk โ†’ text/reasoning blocks), and tool presenters (ctx.tools presentCall/presentResult) render a Think line (latest while running, first when frozen), compact tool rows with real result signals (exit codes / match counts / source counts), and a typewriter answer bubble that streams live and settles in place at turn end โ€” plus the turn summary with thoughts/tools/duration, tokens and cache hit rate (outbound.liveFeed, hot-toggleable)
  • ๐Ÿ“ HTML-aware long sends โ€” messages over 4096 chars are split on newline/space boundaries, never inside tags or entities, with tags rebalanced per part
  • โ™ป๏ธ Reliability-first queue โ€” per-chat FIFO + global sliding-window rate limit; retries only 429/5xx/network/timeout, permanent 4xx fails once; restart-safe long polling with offset preservation
  • ๐Ÿ” Hot update & hot plug โ€” internal/update live-applies whitelist/rules/rate/length/watch without restart; teardown reverses every mount effect and re-apply is idempotent
  • ๐Ÿ›ก๏ธ Safe by default โ€” chat allowlist (empty = deny all), agent tools restricted to the roster, callback payloads percent-encoded, callback tokens single-use and bounded, secrets never ride back
  • ๐Ÿค– Agent tools โ€” telegram_send / telegram_reply / telegram_broadcast / telegram_attach (files: photo/voice/audio/document) / telegram_status / telegram_mark_no_reply, all routed through the same audited send pipeline

Quick Start

1. Create a Telegram bot

Open @BotFather, send /newbot, and keep the returned token.

2. Install the plugin

The package ships its own bundle patch (dsh.bundle.patch โ†’ cordis.patch.yml), so the official CLI installs and mounts it in one step โ€” no profile file edits:

# from a local checkout (link: keeps the profile pointed at your working tree)
dsh plugin --profile <name> add link:/path/to/dsh-telegram

# or from npm once published
dsh plugin --profile <name> add dsh-telegram

3. Provide the token

Resolution order at mount time โ€” first hit wins, values are never logged or persisted by the plugin:

  1. TELEGRAM_BOT_TOKEN environment variable (recommended);
  2. TELEGRAM_BOT_TOKEN_FILE โ€” path to a file whose whole contents are the token;
  3. .telegram-token beside the package (gitignored checkout-local fallback): write the token into <package>/.telegram-token, keep it 0600.
export TELEGRAM_BOT_TOKEN='123456:ABC...'   # option 1
# or: printf '%s\n' '123456:ABC...' > /path/to/dsh-telegram/.telegram-token && chmod 600 $_

3. Configure telegram.json

At <workspace>/.pi/telegram.json (the nearest ancestor directory containing .pi):

{
  "security": { "allowedChatIds": [123456789] },
  "watch": { "autoStart": true },
  "outbound": { "liveFeed": true },
  "interactive": { "userQuestions": "both" }
}

All fields are optional; security.allowedChatIds empty means deny all inbound traffic.

4. Start and allow

/telegram start        # begin long polling (or rely on watch.autoStart)
/telegram allow <id>   # whitelist your chat id

Then send /start to the bot in Telegram. An unauthorized chat that sends /start first gets an Allow button โ€” after tapping it, the welcome message is replayed automatically.

5. Chat

Send a message. The bot binds the chat to its own dsh session, streams the turn (when outbound.liveFeed is on), and replies as a native Telegram quote; no feedback keyboard is attached to the reply.

Configuration

FieldDefaultDescription
security.allowedChatIds[]Inbound whitelist; empty denies all inbound traffic
watch.autoStartfalseStart long polling when an agent is created
inbound.defaultModeauto-handleauto-handle / queue-only / muted
inbound.rules[]Ordered rules on chatId and/or case-insensitive pattern
outbound.parseModeHTMLTelegram parse mode for assistant replies. Model Markdown (bold/italic/code/links/lists/headings/quotes) is normalized to valid HTML automatically; internal cards are always HTML
outbound.disableNotificationfalseSend silently
outbound.maxRetries3Retry attempts for transient failures only
outbound.sendRatePerSecond20Global sliding-window rate limit
outbound.maxMessageLength4096Telegram HTML message limit, used by the splitter
outbound.liveFeedtrueHarness-native streaming feed (built-in; the legacy loader entry keeps working as an alias)
workspace.activePathโ€”Active project folder for new sessions
mode.nameโ€”Profile mode label
model.provider / model.modelโ€”Telegram-owned default model, inherited by /new and โœจ New
reasoning.effortmediumminimal / low / medium / high / max directive prefix
interactive.userQuestionsbothask_user_question ownership: telegram / web / both / auto (both is the default). both shows approval/question asks on Telegram and the web when the host web api-proxy is mounted; answering on either side settles the other automatically, and if api-proxy is unavailable it falls back to Telegram-only. telegram keeps working in the web profile even when the API proxy owns the user-questions provider seam; web yields to the browser UI; auto keeps the legacy loader-entry inference
interactive.allowByTool[]Tool names permanently auto-allowed after the user taps Allow forever (by tool) on an approval card (e.g. ["bash", "web_search"]); set to [] to revoke all

Live updates: Telegram-side /config get|set <path> [json] or dsh-side /telegram config get|set <path> <json> hot-apply and persist any leaf (e.g. outbound.sendRatePerSecond). interactive.userQuestions is read at plugin mount and applies on the next restart; interactive.allowByTool hot-applies immediately.

Architecture

Telegram โ‡„ grammY long polling โ‡„ per-chat FIFO router
                                   โ”‚
                                   โ”œโ”€ Bridge        per-chat bindings, inbound quoting, turn events, reminders
                                   โ”œโ”€ Transport     send queue, rate limit, retry classification, HTML split, stop/start generations
                                   โ”œโ”€ Cards         ephemeral menu/session/model/workspace/goal/... keyboards
                                   โ”œโ”€ Interactive   approval/question cards (settle in place)
                                   โ”œโ”€ Adapters      web ApiProxy parity over ctx services
                                   โ””โ”€ Extensions    reasoning directive + harness-feed streaming renderer
LayerFilesResponsibility
bridgesrc/harness/bridge.tsPer-chat agent routing, event fan-in, native reply quoting, live-feed gate
transportsrc/telegram/transport.tsLong polling, send queue, timeouts, photo/document delivery
queuesrc/telegram/queue.tsPer-chat FIFO + global sliding window + transient-only retries
routersrc/telegram/router.tsPer-chat FIFO for commands/bar/text/callback/media, unauthorized gating
htmlsrc/telegram/html.tsEscaping helpers + HTML-aware long-message splitter
keyboardsrc/telegram/keyboard.tsPure builders for bar/menu/cards, encoded callback payloads
tokenssrc/telegram/tokens.tsBounded single-use callback token registry
adapterssrc/harness/adapters/sessions, workspaces, goals, skills, subagents, presets, settings, credentials, llm, host, jobs, plugins, status
extensionssrc/extensions/reasoning (effort directives) and harness-feed (streaming renderer; openclaw is a deprecated alias)
entrysrc/index.tsapply/teardown, dsh commands + agent tools, card dispatch, hot config

Commands

dsh side

/telegram status ยท /telegram start ยท /telegram stop ยท /telegram allow <chatId> ยท /telegram disallow <chatId> ยท /telegram watch on|off ยท /telegram config auto-start ยท /telegram config get|set <path> [json]

Telegram side

/start /menu /new /compact /stop /models /sessions /workspaces /project [path] /goals /todo /bar [on|off] /skills /subagents /presets /plugins /hostsettings /credentials /host /jobs /status /help /menucheck /answer /config get|set <path> [json]

Plus /history [id] [turns] (the web's turn-grouped trajectory ledger: per-turn model, outcome, duration, and ๐Ÿ‘ค/๐Ÿง /๐Ÿ”ง/๐Ÿ“ฅ/๐Ÿค– steps), /rename <title>, /fork [atSeq], /use <id>, /archive <id>, /queue, /todo, /steer <text>, /cancel, /goal <objective> [maxRounds], /goalcreate <objective> [maxRounds], /goaledit <text>, /goalclear, /workspacecreate <path> [title], /workspacepin <workspaceId> <sessionId> [before], /pluginenable|plugindisable <name>, /settingsdescribe [ns], /settingsupdate <ns> <json>, /settingsreplace <ns> <json>, /settingsmutate <ns> <json-ops>, /credential|credentialset|credentialunset <REF> [value], /ls [path], /mkdir <path>, /openpath [path], /pickdir [path], /discover <settingsNs> [baseURL], /subagentprompt <text>, /sessionlog [id], /commands, /capabilities, and /pluginadd [json] (install your own dynamic plugin from the phone: the host half can call your own model to decode; run/stop/remove live on the Dynamic card, #50).

How It Works

A turn's full lifecycle:

  1. Telegram delivers an update โ†’ transport answers callbacks first, then the per-chat FIFO router dispatches command / bar button / callback / text / photo / document.
  2. The first message in a chat creates and binds a chat-owned dsh session; the FIFO promise spans the whole create โ†’ bind โ†’ deliver path, so a burst of first messages still lands in one session.
  3. The bridge delivers the message as a user turn (or queues it per inbound mode) and records the Telegram message id for native reply quoting.
  4. Whenever outbound.liveFeed is on, the built-in harness-native feed streams the turn the same way the web UI does: Think + tool rows on one activity card, prose typing into its own quoted answer bubble that settles in place (a loader-mounted dsh-telegram/extensions/openclaw entry is still honored as an alias โ€” the singleton guard prevents double mounting).
  5. The final assistant text lands as a native reply to the triggering message with no feedback keyboard attached; a missing telegram_reply produces an explicit reminder instead of silence.
  6. Approval/question cards are claimed by the bridge, answered inline, and settle by editing the card in place (buttons removed).

Tests

npm run check          # tsc build + node --test: 244/244 green
npm audit --omit=dev   # 0 vulnerabilities
npm pack --dry-run     # publish payload: dist + README + README.zh + CHANGELOG + LICENSE

The suite covers bridge routing, multi-chat isolation, transport races, queue retry classification, HTML splitting, keyboard payload encoding, token single-use, interactive cards, config hot-update, every web-parity adapter, and apply-level integration tests (including the rapid-first-message race).

Docs

Safety

  • Only whitelisted chats are handled; an empty allowlist denies all inbound traffic
  • Agent tools can only target chats in the allowed roster
  • Callback payloads are percent-encoded and decode safely; tokens are single-use and bounded
  • User/agent content is always HTML-escaped before wrapping; long HTML is split without breaking tags
  • The bot token is read only from the documented chain (env โ†’ TELEGRAM_BOT_TOKEN_FILE โ†’ gitignored .telegram-token); the plugin itself never writes it anywhere and never logs it; credential values never ride back
  • Permanent Telegram 4xx errors are never retried; a per-call timeout keeps a hung request from wedging the send chain

License

MIT