Skip to content

feat: gateway to chat to goose - telegram etc#7199

Merged
michaelneale merged 30 commits intomainfrom
gateway-abstraction
Feb 25, 2026
Merged

feat: gateway to chat to goose - telegram etc#7199
michaelneale merged 30 commits intomainfrom
gateway-abstraction

Conversation

@michaelneale
Copy link
Copy Markdown
Collaborator

@michaelneale michaelneale commented Feb 13, 2026

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

Screenshot 2026-02-17 at 11 51 22 am Screenshot 2026-02-17 at 11 51 19 am Screenshot 2026-02-17 at 11 51 11 am

and on telegram:

1151 A

moves things into a Gateway tab

- 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)
@michaelneale michaelneale marked this pull request as ready for review February 17, 2026 00:58
Copilot AI review requested due to automatic review settings February 17, 2026 00:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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::Gateway enum 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,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not happy about this but seems needed weirdly- open to feedback

@zanesq
Copy link
Copy Markdown
Contributor

zanesq commented Feb 17, 2026

Is it possible to get cli version of this at the same time?

@michaelneale
Copy link
Copy Markdown
Collaborator Author

@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)
  ...
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated no new comments.

@zanesq
Copy link
Copy Markdown
Contributor

zanesq commented Feb 20, 2026

@michaelneale will take it for a spin tomorrow!

Copilot AI review requested due to automatic review settings February 20, 2026 01:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 1 comment.

Copy link
Copy Markdown
Collaborator

@alexhancock alexhancock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a nice way to start with control from various mobile / messaging apps

sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS gateway_pairings (
platform TEXT NOT NULL,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@zanesq
Copy link
Copy Markdown
Contributor

zanesq commented Feb 20, 2026

Still testing but first impressions:

  1. What is the bot father and why are we using it? Maybe some explanation is needed do I have to use botfather can I trust it? Also, when I click the link it takes me to this page but clicking the button "start bot" doesn't do anything, maybe because I don't have telegram installed on my mac.
image
  1. Should Telegram be moved into a general "Gateway" panel so we can add more chat providers under it?
image

@zanesq
Copy link
Copy Markdown
Contributor

zanesq commented Feb 20, 2026

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)
  ...
@michaelneale
Copy link
Copy Markdown
Collaborator Author

Still testing but first impressions:

  1. What is the bot father and why are we using it? Maybe some explanation is needed do I have to use botfather can I trust it? Also, when I click the link it takes me to this page but clicking the button "start bot" doesn't do anything, maybe because I don't have telegram installed on my mac.
image 2. Should Telegram be moved into a general "Gateway" panel so we can add more chat providers under it? image

@zanesq yeah that is apparently the official way! https://core.telegram.org/bots/features#botfather

@michaelneale
Copy link
Copy Markdown
Collaborator Author

@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.

@michaelneale
Copy link
Copy Markdown
Collaborator Author

take a look again when you want @zanesq

@zanesq
Copy link
Copy Markdown
Contributor

zanesq commented Feb 24, 2026

Will try it soon on my personal computer, juggling work and hack week

@michaelneale michaelneale added this pull request to the merge queue Feb 25, 2026
Merged via the queue into main with commit 19dc192 Feb 25, 2026
18 checks passed
@michaelneale michaelneale deleted the gateway-abstraction branch February 25, 2026 06:55
zanesq added a commit that referenced this pull request Feb 25, 2026
…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)
u35tpus pushed a commit to u35tpus/goose that referenced this pull request Feb 26, 2026
@billybagginz
Copy link
Copy Markdown

will this eventually allow connection to discord?
at first I hated messaging openclaw in discord, but now I love it. I've set up a bunch of different categories, channels, and threads that have really helped organize me parallel conversations and chats. It feels like it is more coherent because even when the server session gets compacted or restarted, all of the context is in the discord discussions, and I can just refer to specific channels or threads for the session to get up to speed. And it kind of offloads memories into Discord discussions.

blackgirlbytes added a commit that referenced this pull request Mar 17, 2026
- 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants