Skip to content

feat: unfreeze TelegramBridgeConfig for hot-reload of voice, files, chat_ids, timing settings #286

@nathanschram

Description

Context

PR #285 (issue #269) implemented hot-reload for trigger configuration (crons/webhooks) via TriggerManager. During that work, we identified a second class of settings that could be hot-reloadable but are blocked by the frozen TelegramBridgeConfig dataclass.

Problem

TelegramBridgeConfig is defined as @dataclass(frozen=True, slots=True) in src/untether/telegram/bridge.py:134. This means all fields are immutable after construction. Several of these fields are already read per-message (correct pattern for hot-reload), but the frozen container prevents updating them when the TOML changes.

Settings blocked by frozen config

Setting Field(s) Read pattern Effort
Allowed users allowed_user_ids Per-message in route_update() Easy
Chat routing chat_ids Per-poll via _allowed_chat_ids() Easy
Voice transcription voice_transcription, voice_max_bytes, voice_transcription_model, voice_transcription_base_url, voice_transcription_api_key, voice_show_transcription Per-message in voice handler Easy
File transfer files (TelegramFilesSettings) Per-message in file handler Easy
Message timing forward_coalesce_s, media_group_debounce_s Copied to state at startup Easy — reference config instead
Resume line show_resume_line Per-message (already effectively reloadable via run_options) Already done

Proposed approach

Option A: Unfreeze the dataclass

  • Remove frozen=True from TelegramBridgeConfig
  • Add an update_from() method that selectively updates reloadable fields
  • Wire into handle_reload() in loop.py
  • Pros: Simple, minimal code changes
  • Cons: Loses immutability guarantees

Option B: Config wrapper / proxy

  • Keep TelegramBridgeConfig frozen
  • Create a LiveConfig wrapper that holds a mutable reference to the latest config
  • Components read from wrapper instead of frozen config
  • Pros: Keeps immutability for non-reloadable fields
  • Cons: More indirection

Recommendation: Option A is simpler and sufficient. The fields that truly can't change (bot token, transport) are architectural constraints, not protected by frozen=True.

What remains restart-only (no fix possible)

  • Bot token / chat ID — Telegram client connection
  • Webhook server host/port — aiohttp binds once
  • Transport type — entire transport stack
  • session_mode (stateless ↔ chat) — requires state store init/teardown
  • topics.enabled toggle — requires topic state store init
  • New engine binaries — resolved via shutil.which() at startup

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions