feat: gateway to chat to goose - telegram etc#7199
Conversation
- Add SessionType::Gateway to goose core crate
- Create gateway/ module in goose-server with:
- mod.rs: Core types (PlatformUser, IncomingMessage, OutgoingMessage,
PairingState, GatewayConfig) and Gateway trait
- handler.rs: GatewayHandler bridges platform messages to goose sessions,
handles full pairing flow and relays messages via agent.reply()
- manager.rs: GatewayManager with registry pattern for running gateway
instances, start/stop lifecycle, pairing code generation
- pairing.rs: SQLite-backed PairingStore with in-memory cache,
code generation (6-char alphanumeric), expiry handling
- All tests passing, clippy clean, formatted
- Add telegram.rs: TelegramGateway with long-polling, message splitting, typing indicators, bot validation via getMe - Refactor GatewayHandler to hold Arc<dyn Gateway> instead of taking &dyn Gateway per call — enables spawning handler tasks per message - Add create_gateway() factory function in mod.rs - Add GatewayStatus/PairedUserInfo types and status() method to manager - Enhance pairing store list_paired_users to include paired_at timestamp - Add unpair_user and already-running check to manager - 14 tests passing (9 telegram split_message + 5 pairing)
- Add routes/gateway.rs with 5 endpoints:
POST /gateway/start, POST /gateway/stop, GET /gateway/status,
POST /gateway/pair, DELETE /gateway/pair/{platform}/{user_id}
- Add GatewayManager to AppState, initialized in AppState::new()
- Register gateway routes in routes/mod.rs configure()
- Add mod gateway to main.rs for binary target
- All 14 tests passing, clippy clean, formatted
- Add GatewaySettingsSection.tsx with full gateway management: - Gateway list with running status and paired user count - Add Gateway modal (Telegram bot token input with BotFather link) - Stop gateway button - Generate Pairing Code with countdown timer modal - Paired users list with unpair button per user - Auto-refresh status every 5 seconds - Add 'Gateways' tab to SettingsView between Keyboard and App - Regenerate openapi.json and types.gen.ts for SessionType::Gateway - TypeScript clean, ESLint clean
The gateway handler was creating sessions and calling agent.reply() without ever setting up a provider, causing 'provider not set' errors. Two changes: 1. complete_pairing() now stores the current provider name, model config, and default extensions on the session at pairing time — matching what start_agent does for regular sessions. 2. relay_to_session() now calls restore_provider_from_session() and load_extensions_from_session() before agent.reply() — handling both fresh agents and agents evicted from the LRU cache.
Gateway configs were only held in-memory and lost on app restart.
Changes:
- GatewayManager now persists configs using goose's config system:
- Non-secret gateway metadata (gateway_type, max_sessions) stored via
Config::set_param('gateway_configs') as a YAML list
- Platform secrets (bot tokens etc) stored via Config::set_secret()
keyed as 'gateway_platform_config_{type}'
- start_gateway() saves config after successful start
- stop_gateway() removes saved config (gateway won't auto-start)
- check_auto_start() loads saved configs and starts gateways on launch
- Server startup (commands/agent.rs) spawns gateway auto-start task
alongside the existing tunnel auto-start
- Remove standalone Gateways tab from SettingsView.tsx - Render GatewaySettingsSection in AppSettingsSection.tsx right after TunnelSection — both connectivity features in the same place - GatewaySettingsSection checks GET /tunnel/status on mount and returns null when tunnel state is 'disabled' (GOOSE_TUNNEL=no), matching TunnelSection's visibility behavior
GatewaySettingsSection was checking tunnel status independently using
gatewayFetch('/tunnel/status') which may have silently failed, causing
the component to not render.
Fix: moved the tunnel-disabled check into AppSettingsSection using the
SDK's getTunnelStatus() (same method TunnelSection uses). Both
TunnelSection and GatewaySettingsSection are now conditionally rendered
together by the parent — neither renders when GOOSE_TUNNEL=no.
- Add Gateway tab with Radio icon between Keyboard and App tabs - Gateway tab contains TunnelSection + GatewaySettingsSection - Hide Gateway tab entirely when tunnel is disabled (GOOSE_TUNNEL=no) - Remove TunnelSection/GatewaySettingsSection from AppSettingsSection - Check tunnel status on mount via getTunnelStatus API
…state - Default tunnelDisabled to false so tab shows immediately - Only hide when API explicitly returns state: 'disabled' - On API error/timeout, keep tab visible (fail open) - Simplify conditionals from === false to !tunnelDisabled
Gateway sessions are long-lived, but the user may change their provider, model, or extensions in the desktop settings at any time. Previously the session was configured once at pairing time and never updated, so gateway chats could use stale config (wrong provider, missing extensions). Now, on each incoming Telegram message, relay_to_session calls sync_session_config which: 1. Reads the current global config (provider, model, extensions) 2. Compares with what's stored on the session 3. If anything changed, updates the session in the DB 4. If extensions changed, tears down the old agent (removing stale extension processes) so a fresh agent is created After sync, the session is re-read and restore_provider_from_session + load_extensions_from_session pick up the current values. When nothing has changed the sync is a no-op — no DB writes, no agent recreation.
…ream Two issues fixed in relay_to_session: 1. Typing indicator keepalive: Telegram stops showing 'typing...' after ~5 seconds. When the agent is doing tool calls (file listing, etc.), the stream can take much longer. Now a background task re-sends the typing indicator every 4 seconds, cancelled when the stream finishes or errors. This ensures the user always sees activity in the chat. 2. Debug logging: Added structured debug logs for every AgentEvent in the stream — message events (with role + content count), tool requests/responses, MCP notifications, model changes, history replacements, and stream completion (with event count + response length). This makes it possible to diagnose 'no response' issues from server logs. Also: the stream loop now emits ToolStarted/ToolCompleted messages to the gateway for assistant ToolRequest/ToolResponse content. For Telegram this translates to additional typing indicators (ToolStarted) which further keeps the UI responsive during multi-step tool interactions.
Two fixes for multi-turn tool-calling sessions feeling unresponsive: 1. Set max_turns to 5 for gateway sessions. Previously None (default 1000), so the agent could do dozens of tool-call round-trips before responding. Now after 5 LLM→tool turns it stops and replies with what it has, asking the user if they want to continue. 2. Send assistant text to Telegram as it arrives instead of buffering everything until the stream ends. When a ToolRequest follows text in the same message, the text is flushed immediately — the user sees 'Let me check...' right away, then the typing indicator while tools run, then the next response as a separate message. Only the final trailing text (after the last tool round-trip) waits for stream end. Before: user waits 30+ seconds seeing only 'typing', gets one giant message at the end (or nothing if it times out). After: user sees incremental messages as the agent works through each step, with clear activity indicators between them.
* origin/main: (42 commits) fix: use dynamic port for Tetrate auth callback server (#7228) docs: removing LLM Usage admonitions (#7227) feat(otel): respect standard OTel env vars for exporter selection (#7144) fix: fork session (#7219) Bump version numbers for 1.24.0 release (#7214) Move platform extensions into their own folder (#7210) fix: ignore deprecated skills extension (#7139) Add a goosed over HTTP integration test, and test the developer tool PATH (#7178) feat: add onFallbackRequest handler to McpAppRenderer (#7208) feat: add streaming support for Claude Code CLI provider (#6833) fix: The detected filetype is PLAIN_TEXT, but the provided filetype was HTML (#6885) Add prompts (#7212) Add testing instructions for speech to text (#7185) Diagnostic files copying (#7209) fix: allow concurrent tool execution within the same MCP extension (#7202) fix: handle missing arguments in MCP tool calls to prevent GUI crash (#7143) Filter Apps page to only show standalone Goose Apps (#6811) opt: use static for Regex (#7205) nit: show dir in title, and less... jank (#7138) feat(gemini-cli): use stream-json output and re-use session (#7118) ...
When a gateway is stopped (stop_gateway or stop_all), all pairings for that platform are removed from the database. Users must re-pair on next start. Makes sense for Telegram where pairings are tied to a running goose session.
Stop now keeps the saved config (bot token) so the gateway can be restarted without re-entering credentials. Pairings are still cleared on stop so users must re-pair. Three states in the UI: - Unconfigured: shows token input + Start - Configured + stopped: shows Start button + trash to remove - Running: shows Pair Device + Stop New API endpoints: - POST /gateway/restart - restart a stopped gateway from saved config - POST /gateway/remove - stop + delete saved config entirely Status endpoint now includes configured-but-stopped gateways.
Cleaned up the telegram card to three clear states: - No config: shows token input + Start - Stopped (has saved config): shows Start + trash to remove - Running: shows Pair Device + Stop Start after stop calls /gateway/restart which uses the saved token. No need to re-enter it.
* main: fix: handle reasoning_content for Kimi/thinking models (#7252) feat: sandboxing for macos (#7197) fix(otel): use monotonic_counter prefix and support temporality env var (#7234) Streaming markdown (#7233) Improve compaction messages to enable better post-compaction agent behavior (#7259) fix: avoid shell-escaping special characters except quotes (#7242)
There was a problem hiding this comment.
Pull request overview
This PR introduces a gateway system for chatting with goose through external platforms, starting with Telegram. Users can pair their Telegram bot with goose using a secure 6-character pairing code that expires after 5 minutes. The system manages long-running, self-compacting sessions similar to Slack/Discord bot interactions. The UI has been reorganized to move the tunnel section from "App" settings to a new "Gateway" tab (which is hidden when tunnel functionality is disabled).
Changes:
- Adds gateway infrastructure (manager, handler, pairing store with SQLite persistence)
- Implements Telegram bot integration with markdown-to-HTML formatting
- Creates new Gateway tab in UI settings (conditionally shown based on tunnel availability)
- Adds new
SessionType::Gatewayenum variant - Installs rustls crypto provider early in server startup to support TLS in tunnel and gateway connections
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
crates/goose-server/src/gateway/mod.rs |
Gateway trait definitions and platform abstractions |
crates/goose-server/src/gateway/manager.rs |
Gateway lifecycle management, config persistence |
crates/goose-server/src/gateway/handler.rs |
Message routing, pairing flow, agent integration |
crates/goose-server/src/gateway/pairing.rs |
SQLite-based pairing state persistence |
crates/goose-server/src/gateway/telegram.rs |
Telegram Bot API integration |
crates/goose-server/src/gateway/telegram_format.rs |
Markdown to Telegram HTML converter |
crates/goose-server/src/routes/gateway.rs |
HTTP API endpoints for gateway operations |
crates/goose-server/src/state.rs |
Adds GatewayManager to app state |
crates/goose-server/src/commands/agent.rs |
Installs rustls provider, auto-starts gateways |
crates/goose-server/src/tunnel/lapstone.rs |
Removes duplicate rustls provider installation |
crates/goose/src/session/session_manager.rs |
Adds Gateway session type |
ui/desktop/src/components/settings/gateways/GatewaySettingsSection.tsx |
New UI component for gateway management |
ui/desktop/src/components/settings/SettingsView.tsx |
Adds Gateway tab, conditionally shown |
ui/desktop/src/components/settings/tunnel/TunnelSection.tsx |
Renames "Remote Access" to "Mobile App" |
ui/desktop/src/components/settings/app/AppSettingsSection.tsx |
Removes tunnel section (moved to Gateway tab) |
crates/goose-server/Cargo.toml |
Adds sqlx, pulldown-cmark dependencies |
The 'tidying up' commit accidentally removed the rustls::crypto::ring::default_provider().install_default() call from run_single_connection, which the tunnel WebSocket connection needs to establish TLS.
| } | ||
|
|
||
| pub async fn run() -> Result<()> { | ||
| // Install the rustls crypto provider early, before any spawned tasks (tunnel, |
There was a problem hiding this comment.
not happy about this but seems needed weirdly- open to feedback
|
Is it possible to get cli version of this at the same time? |
|
@zanesq yeah I was deliberately keeping it in desktop for the experience, just easier to test, but I guess could as well on cli? |
* main: (54 commits) docs: add monitoring subagent activity section (#7323) docs: document Desktop UI recipe editing for model/provider and extensions (#7327) docs: add CLAUDE_THINKING_BUDGET and CLAUDE_THINKING_ENABLED environm… (#7330) fix: display 'Code Mode' instead of 'code_execution' in CLI (#7321) docs: add Permission Policy documentation for MCP Apps (#7325) update RPI plan prompt (#7326) docs: add CLI syntax highlighting theme customization (#7324) fix(cli): replace shell-based update with native Rust implementation (#7148) docs: rename Code Execution extension to Code Mode extension (#7316) docs: remove ALPHA_FEATURES flag from documentation (#7315) docs: escape variable syntax in recipes (#7314) docs: update OTel environment variable and config guides (#7221) docs: system proxy settings (#7311) docs: add Summon extension tutorial and update Skills references (#7310) docs: agent session id (#7289) fix(gemini-cli): restore streaming lost in #7247 (#7291) Update more instructions (#7305) feat: add Moonshot and Kimi Code declarative providers (#7304) fix(cli): handle Reasoning content and fix streaming thinking display (#7296) feat: add GOOSE_SUBAGENT_MODEL and GOOSE_SUBAGENT_PROVIDER config options (#7277) ...
|
@michaelneale will take it for a spin tomorrow! |
alexhancock
left a comment
There was a problem hiding this comment.
Looks like a nice way to start with control from various mobile / messaging apps
crates/goose/src/gateway/pairing.rs
Outdated
| sqlx::query( | ||
| r#" | ||
| CREATE TABLE IF NOT EXISTS gateway_pairings ( | ||
| platform TEXT NOT NULL, |
There was a problem hiding this comment.
should platform be free text or an enum-like thing?
maybe add a CHECK constraint or a separate reference table with a foreign key to a list of things we support?
There was a problem hiding this comment.
hrm, I wonder if I even need to have this, it is just a setting, it shouldn't be that special, I think might tidy this up and simplify the code, good catch. There was some time I was thinking of treating the sessions as special (as with a gateway you tend to have a long running session which runs basically for ever - and if we follow the openclawd pattern, a 30 minute heartbeat to check if there is any thing to follow on, notify user with a push etc) - but even that can be done without needing special info in the DB I think
|
Dang Telegram app blocked by corp policy. I can try this on my personal box this weekend. |
* main: (27 commits) dev: add cmake to hermitized env (#7399) refactor: remove allows_unlisted_models flag, always allow custom model entry (#7255) feat: expose context window utilization to agent via MOIM (#7418) Small model naming (#7394) chore(deps): bump ajv in /documentation (#7416) doc: groq models (#7404) Client settings (#7381) Fix settings tabs getting cut off in narrow windows (#7379) docs: voice dictation updates (#7396) [docs] Add Excalidraw MCP App Tutorial (#7401) Post release checklist as a comment on release PRs (#7307) unique api key (#7391) fix: use correct colors for download progress bar (#7390) Add local model settings access from bottom bar model menu (#7378) Change Recipe Security Scanner API key (#7387) switch Ask AI Discord bot from openrouter to anthropic (#7386) feat(ui): show token counts directly for "free" providers (#7383) Update creator note (#7384) Remove display_name from local model API and use model ID everywhere (#7382) fix(summon): stop MOIM from telling models to sleep while waiting for tasks (#7377) ...
@zanesq yeah that is apparently the official way! https://core.telegram.org/bots/features#botfather |
|
@zanesq yeha I had it in a gateway panel, but we don't have a scalable design for settings so it just added a lot of clutter when I tried (originally) but yeah when we have more can put mobile and it and other things in there. |
|
take a look again when you want @zanesq |
|
Will try it soon on my personal computer, juggling work and hack week |
…m-cache * 'main' of github.com:block/goose: chore(release): release version 1.25.0 (minor) (#7263) Docs: Community all-stars and page update (#7483) fix: searchbar zindex modal overlay (#7502) blog: Order Lunch Without Leaving Your AI Agent (#7505) feat: gateway to chat to goose - telegram etc (#7199) Option to add changeable defaults in goose-releases (#7373) Fix out of order messages (#7472) fix: ensure latest session always displays in sidebar (#7489) docs: rename TLDR to Quick Install in MCP tutorials (#7493) docs: update Neighborhood extension page with video embed and layout improvements (#7473) feat: let AskAI Discord bot see channels in the server (#7480) Apps token limit (#7474) fix(apps): declare color-scheme to allow transparent MCP App iframes (#7479)
Signed-off-by: Oleg Levchenko <[email protected]>
|
will this eventually allow connection to discord? |
- Create new remote-access/ section under experimental/ - Add Telegram Gateway documentation covering setup, CLI usage, and troubleshooting - Move mobile-access.md into remote-access/ section - Update experimental index to link to new Remote Access section - Fix broken links in blog post and goose-mobile.md Related to: #7199 Signed-off-by: Rizel Scarlett <[email protected]>




This introduces a gateway concept - first off the rank is telegram chat:
you pair up goose with telegram bot (and send a pairing code to secure it) - and you can chat with goose with a long running self compacting session (similiar to claudbot/openbotclawd(.*)awd) thing, but with the desktop
and on telegram:
moves things into a Gateway tab