Skip to content

feat: hot-reload support for triggers, watchdog, and progress settings #269

@nathanschram

Description

Summary

Explore which parts of Untether can be dynamically updated via config reload (watch_config) without requiring a full bot restart. Currently, editing untether.toml and restarting the service is the only way to apply changes to several subsystems that could reasonably support hot-reload.

Motivation

Adding a new cron trigger, adjusting watchdog timeouts, or tweaking progress settings currently requires systemctl --user restart untether-dev — which kills all active runs. For a Telegram-first workflow, being able to edit config and have it apply live is a significant UX improvement.

Current state

Already hot-reloadable (no work needed)

  • Projects config — chat-to-project mappings update on config change
  • Per-chat preferences — engine, plan mode, verbose, etc. (JSON state files, reloaded on-demand)
  • Preamble — re-read from TOML on every run via _load_preamble_settings()
  • Per-chat/topic engine overrides — model, reasoning (persistent JSON, on-demand reload)
  • Command menuhandle_reload() already calls refresh_commands() + set_command_menu()

Needs work

Area Current behaviour What hot-reload would look like Effort
Triggers (cron + webhooks) Parsed once at startup in loop.py:1488-1525; run_cron_scheduler() loops forever with a fixed crons list on_reload callback cancels old scheduler/webhook tasks, spins up new ones with fresh config; preserve last_fired state to avoid double-firing Medium-hard
Watchdog settings WatchdogSettings read at startup, baked into runner bridge Read tool_timeout, mcp_tool_timeout from current settings per-run instead of caching Easy
Progress settings ProgressSettings (verbosity, max_actions, min_render_interval) read at startup Fetch from runtime on each ProgressEdits construction or per-render cycle Easy
Footer settings FooterSettings read at startup Fetch per-render from runtime Easy
Cost/budget settings CostSettings read at startup Re-read thresholds per-run from runtime; active CostTracker picks up new limits Easy-medium

Out of scope (entry-point limitation)

  • Engine registration — Python entry points require package reinstall
  • Command registration — same entry-point constraint
  • These would need a fundamentally different plugin discovery mechanism

Implementation sketch

Triggers (the big one)

  1. Wrap cron scheduler + webhook server tasks in a TriggerManager that holds CancelScope references
  2. On on_reload, if trigger config changed: cancel old scopes, start new scheduler/server tasks
  3. Carry forward last_fired dict to avoid re-firing within the same minute
  4. For webhooks: graceful close of old HTTP listener before binding new one (or skip if routes unchanged)

Settings per-run

For watchdog, progress, footer, and cost settings:

  1. Store the TransportRuntime reference (already available in runner bridge)
  2. Read settings from runtime.settings at the point of use rather than caching at init
  3. Minimal code change — mostly replacing self._timeout with self._runtime.settings.watchdog.tool_timeout

Acceptance criteria

  • Editing [triggers] in untether.toml with watch_config = true applies new cron/webhook config without restart
  • Editing [watchdog] timeouts applies to the next run without restart
  • Editing [progress] settings applies to the next progress render without restart
  • Editing [footer] settings applies to the next completed run without restart
  • Editing [cost] budget thresholds applies to the next run without restart
  • No double-firing of cron triggers during reload transition
  • Existing active runs are not disrupted by config reload

Related

  • config_watch.py — existing file watcher infrastructure
  • handle_reload() in telegram/loop.py:1272 — existing reload callback (currently handles projects + command menu)
  • watch_config: bool = False in settings.py:198 — opt-in flag

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