Telegram remote control for DeepSeek Harness: list conversations with titles and workspaces, resume paused persisted sessions on demand (preset remount), turn-tracked reply relay, first-line reasoning summaries, inline-button approve/reject with outcome edits, and a bot command menu.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:jackControls/dsh-telegram-control
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
Remote-control plugin for DeepSeek Harness (dsh).
Runs a Telegram bot inside the harness process so you can drive your agents, jobs, and harness
status from your phone: send a message to the bot and it becomes a follow-up to your agent, whose
reply comes back to you as a Telegram message.
Everything is a plugin in dsh — this one is a Cordis function plugin that talks to the Telegram Bot API over long-polling with zero runtime dependencies beyond the harness itself.
Features
- Remote agent control — plain text messages are sent as follow-ups to the chat's selected conversation and appear as ordinary user messages in the desktop Web UI conversation. The agent's reply — including its thinking/reasoning blocks, marked with 💭 — is relayed back the moment its turn closes (turn-tracked, so delivery does not depend on the agent ever reporting idle).
- Command surface —
/status,/agents,/agent <session id>,/jobs,/kill <job id>,/cancel,/watch//unwatch,/chatid,/help. - Approval on the phone — harness permission requests (sandbox escalations and other
approval/requestasks) arrive in Telegram with ✅ Allow once / ❌ Reject inline buttons; the answer is applied and the message is edited with the outcome. If Telegram cannot be reached the question falls back to the Web UI dialog instead of failing closed. - Command menu — the bot's slash commands are published via
setMyCommands, so/agents,/agent,/jobs, … show up in the Telegram input field without being typed by hand. - Live push —
/watchforwards every assistant message from live sessions to your chat. - Auth by chat allowlist — unknown chats get an onboarding hint with their chat id, nothing else.
- Safe output — all dynamic text is HTML-escaped before it reaches Telegram; long replies are split into Telegram-sized chunks.
Requirements
dshrunning from the npm package (npx @deepseek-ai/dsh) or a repository checkout.- Node.js with a global
fetch(Node ≥ 18; dsh itself needs Node ≥ 22). - A Telegram bot token from @BotFather.
Installation
Get a bot token: message @BotFather, run
/newbot, and copy the token.Install the plugin into a profile. Either from this repository
dsh plugin --profile web add github:jackControls/dsh-telegram-controlor, from a local checkout:
dsh plugin --profile web add /path/to/dsh-telegram-control(Replace
webwith the profile you run. The package declaresdsh.bundle, sodsh plugin addinstalls it and activates it as a profile layer automatically — you don't need to touchcordis.patch.yml.lib/is committed, so git installs work without a build step.)Configure via environment in the same process that runs
dsh:export DSH_TELEGRAM_TOKEN='123456:ABC-DEF...' export DSH_TELEGRAM_ALLOWED_CHATS='123456789,987654321' # comma-separated chat idsRestart
dsh. Start a private chat with your bot, send/chatidto learn your chat id if you haven't added it yet, then/help.
Manual patch (no dsh plugin add)
If you prefer to mount it by hand, add a row to your profile's cordis.patch.yml
($DSH_HOME/profiles/<profile>/cordis.patch.yml):
- insert:
- id: telegram-control
name: 'dsh-telegram-control'
config:
# optional: pin values here instead of the environment
token: '123456:ABC-DEF...'
allowedChatIds: [123456789]
Configuration
| Config key | Env fallback | Default | Meaning |
|---|---|---|---|
token |
DSH_TELEGRAM_TOKEN |
— (required) | Telegram bot token. |
allowedChatIds |
DSH_TELEGRAM_ALLOWED_CHATS |
[] (deny all) |
Authorized chat ids. Note: schemastery validates a missing array to [], so an empty/absent allowlist always falls back to the environment. |
apiBase |
— | https://api.telegram.org |
Bot API base (useful for proxies and tests). |
defaultAgentId |
— | none | Session id plain messages target when the chat has no /agent selection. |
pollTimeoutSec |
— | 50 |
Long-poll getUpdates timeout (Telegram accepts up to 50). |
replyTimeoutMs |
— | 600000 (10 min) |
Max wait for an agent reply before flushing partial output with a note. |
showToolCalls |
— | false |
Emit one-line 🔧 <name> notices while a reply is pending. |
maxMessageChars |
— | 4000 |
Per-message character cap before Telegram-side splitting. |
Commands
| Command | What it does |
|---|---|
/help, /start |
Command list. |
/status |
Uptime, conversation count (live/total), background job count. |
/agents |
List every conversation — live agents and paused persisted sessions (the same set the Web UI sidebar shows): numbered, named by their session title, each with its workspace in brackets ([~/path]), status (idle/running/paused), model, and a 👈 marker on this chat's selection. |
/agent <number> |
Select the conversation at that position in the /agents list. |
/agent <name> |
Select by a case-insensitive substring of the conversation's title or session id; ambiguous matches list candidates. |
/agent <session id> |
Select by the exact session id. /agent with no argument shows the current selection. |
/jobs |
List background jobs. |
/kill <job id> |
Request a background job be stopped. |
/cancel |
Cancel the selected agent's current turn. |
/watch / /unwatch |
Toggle forwarding live agent output to this chat. |
/chatid |
Show this chat's id (for the allowlist). |
Plain messages are sent as follow-ups to the selected conversation. Selection order: the chat's
/agent choice, then defaultAgentId, then the single conversation if there is exactly one. A
paused (persisted-but-not-live) conversation is resumed on first message exactly like the Web
UI does it — the session's stored agent preset is re-mounted, so history replays under the same
composition it was produced under. Each chat's selection is persisted to
$DSH_HOME/telegram-control-state.json, so it survives harness restarts. Conversation names come
from the harness's session titles (session/title events — the auto-summary or your manual
rename, the same names the Web UI shows); the bracketed workspace is the session's cwd.
How it works
apply(ctx, config)runs a long-pollinggetUpdatesloop inside the harness process (fetch-based, no bot framework). A 409 from the API (another poller) stops that poller cleanly; network errors back off up to 30 s.- Plain messages call
agent.followup(createUserMessage(...))withsource: { kind: 'user' }— the same source the Web UI's own input uses, so the text shows up as a normal user bubble in the desktop conversation. - The plugin listens to the durable
session/eventfeed and the liveagent/inbox/claimed/agent/status/agent/error/agent/disposedevents. Each follow-up's message id is matched againstagent/inbox/claimedto learn its turn number, and the accumulated reply (visible text + 💭 reasoning) is flushed when that turn'sturn/endlands — an idle flush and a timeout note cover the remaining cases. Tool-call and error notices relay as they happen; typing indicators show while the agent runs. - All registrations are Cordis effects, so unloading the plugin (HMR, profile reload) tears the bot down cleanly.
Security
- Every inbound message is authorized before any action. Each update's chat id is checked
against
allowedChatIds(orDSH_TELEGRAM_ALLOWED_CHATS) before anything is dispatched: no commands run, no messages reach an agent, and no approval answers are accepted for an unlisted chat. A storeduserIdis never treated as authorization. - An empty allowlist denies everyone (fail closed): with no chat id configured, every message is rejected and the only reply an unlisted chat ever receives is the onboarding hint that tells it its own chat id.
- The bot is a remote shell into your harness by design: only listed chats may issue commands or answer approvals. Keep the allowlist tight.
- The token is a bearer credential: prefer the environment variable over a committed patch file.
- The plugin does not widen any harness capability — it can only do what your running harness can do, and the harness's own sandbox/approval policies still apply to agent work.
Compatibility, permissions, and verification
- Node.js:
>=22(declared inengines). - DeepSeek Harness:
>=0.1.0-rc.6(declared inpeerDependencies). Verified end to end against bothdsh0.1.0-rc.6 (npm CLI) and 0.1.5-rc.2 (the version bundled by DSH Desktop) — the plugin reads either persistence generation (inspect()oropen(id,'read')), either session-history accessor (eventsorsnapshotEvents()), and eitherlist()shape. - Runtime dependencies: one —
@deepseek-ai/schemastery(config validation). Everything else is apeerDependencyresolved from the host installation; there are no postinstall scripts and no bundled binaries. - Permissions / external services: the plugin reaches the network (Telegram Bot API over
HTTPS), uses a credential (the bot token from
config.tokenor$DSH_TELEGRAM_TOKEN), and writes exactly one state file ($DSH_HOME/telegram-control-state.json) recording which conversation each chat selected. It does not read or write any other local file. - Failure bounds: an unreachable Telegram API backs off and retries; a 409 (another poller) stops that poller; if Telegram cannot receive an approval the Web UI dialog still answers it. Unloading the plugin aborts polling and clears every pending prompt.
- Verification evidence:
npm test(24 unit tests) plusnode tests/smoke.mjs, a two-phase end-to-end run that installs the plugin into a disposable profile, boots a realdsh web, and asserts the live-agent relay, paused-session resume, approval round-trip, and question round-trip (32 checks). SetDSH_CLIto test a specific harness build.
Development
npm install # dev deps (typecheck + build)
npx tsc # typecheck + emit lib/
node --test 'tests/*.test.mjs' # unit tests for the pure helpers
node tests/smoke.mjs # end-to-end: boots a real `dsh web` in an isolated $DSH_HOME,
# with fake Telegram + mock-LLM servers, and asserts the whole
# message→agent→relay loop (set DSH_CLI to your dsh bin if needed)
Known limitations
- Telegram webhooks are not supported; long-polling only (fine for a personal remote control).
- The plugin observes the session event feed; very high-frequency sessions could flood a watching
chat —
/unwatchis your friend. sessionId-keyed reply buffering assumes one user drives one agent; two chats driving the same agent get one combined reply per turn (each chat receives it).
License
MIT
Links
More in this category
xmanrui/dsh-im★ 1420
Connect IM bots to DeepSeek Harness via QR codes or bot credentials (9 channels: Feishu, WeChat, DingTalk, WeCom, QQ, Slack, Telegram, Discord, and WhatsApp).
shaobeichen/dsh-pocket★ 1259
Remote phone access to the DSH Web UI: scan a QR code for LAN or public (cloudflared tunnel) access with real-time sync, a mobile-adaptive layout, and a settings tab.
alvinunreal/openpets#dsh★ 1217
Bridges DeepSeek Harness lifecycle status, errors, and approval requests to a locally running OpenPets desktop companion.
inclusionAI/Avernet#deepseek-harness-channel-bcn★ 557
Connects DeepSeek Harness to Avernet's Bot Collaboration Network over WebSocket V2, with automatic onboarding, isolated agent sessions, tool-call events, and multi-bot routing tools.
omdsh-dev/dsh-notification★ 83
Desktop notifications for turn completions, with per-outcome controls and keyword rules.
omdsh-dev/dsh-lark★ 55
Lark/Feishu bot channel for DeepSeek Harness: each chat drives its own agent, and tool approvals, model questions, and plan reviews return as cards answered by a button or a reply. Switch workspace and model from the chat (`/cd`, `/model`, `/new`), and run several bots that keep separate sessions and can hand turns to each other in one group.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.