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
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 frozenTelegramBridgeConfigdataclass.Problem
TelegramBridgeConfigis defined as@dataclass(frozen=True, slots=True)insrc/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
allowed_user_idsroute_update()chat_ids_allowed_chat_ids()voice_transcription,voice_max_bytes,voice_transcription_model,voice_transcription_base_url,voice_transcription_api_key,voice_show_transcriptionfiles(TelegramFilesSettings)forward_coalesce_s,media_group_debounce_sshow_resume_lineProposed approach
Option A: Unfreeze the dataclass
frozen=TruefromTelegramBridgeConfigupdate_from()method that selectively updates reloadable fieldshandle_reload()inloop.pyOption B: Config wrapper / proxy
TelegramBridgeConfigfrozenLiveConfigwrapper that holds a mutable reference to the latest configRecommendation: 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)
session_mode(stateless ↔ chat) — requires state store init/teardowntopics.enabledtoggle — requires topic state store initshutil.which()at startupRelated