Skip to main content
Guides Last updated: 8 October 2026

Module map

Nathan Schram
By Founder, Little Bear Apps

This page is a high-level map of Untether’s internal modules: what they do and how they fit together.

Entry points

ModuleResponsibility
cli/Typer CLI package (run.py, doctor.py, config.py, init.py, onboarding_cmd.py, plugins.py); loads settings, selects engine/transport, runs the transport backend.
telegram/backend.pyTelegram transport backend: validates config, runs onboarding, builds and runs the Telegram bridge.

Orchestration and routing

ModuleResponsibility
runner_bridge.pyTransport-agnostic orchestration: per-message handler, progress updates, final render, cancellation, resume coordination.
router.pyAuto-router: resolves resume tokens by polling runners; selects a runner for a message.
scheduler.pyPer-thread FIFO job queueing with serialisation.
directives.py, context.py, worktrees.pyDirective parsing (/<engine>, /<project>, @branch, dir: lines), run context types, and branch worktree creation.
transport_runtime.pyFacade used by transports and commands to resolve messages and runners without importing internal router/project types.
cost_tracker.pyPer-run and daily cost tracking with budget alerts (checked when a result arrives; the daily gate before a run lives in budget_gate.py).
budget_gate.py[cost_budget] auto_cancel (“Stop at limit”): refuses new runs once today’s total reaches max_cost_per_day, until local midnight (#896).
session_stats.pyPer-engine run counts, actions and durations behind /stats.
error_hints.pyMaps engine error text to the actionable hints shown in Telegram.
shutdown.pyGraceful shutdown state and drain logic.
live_followup.pyWrites a queued follow-up into a still-running Claude session (live sessions) instead of resuming a new process.
session_costs.pyPer-session cost and token ledger, so a resumed session’s running totals are recorded per run.
session_quarantine.pyPersisted markers for sessions that must not be resumed (empty-resume recovery).
permission_audit.pyStartup / reload audit of Claude permission modes and crons that would wait for an approval tap.
telegram/at_scheduler.pyOne-shot delayed runs from /at <duration>; in-memory state, drained on shutdown.
loop_scheduler.pyLoop mode: runs Claude’s /loop schedules (declined CronCreate calls) and long ScheduleWakeups with the [loop] caps, plus the cancel and cron-suppression records; persists active_loops.json so loops survive restart. Mirrors at_scheduler API.
orphan_approvals.pyPushed standalone Approve / Deny message for a background agent’s approval request that arrives while a Claude session is idle; re-sent after 10 min, then every 30 min, and removed once answered.

Domain model and events

ModuleResponsibility
model.pyDomain types: resume tokens, events, actions, run results.
runner.pyRunner protocol and event queue utilities.
events.pyEvent factory helpers for building Untether events consistently.

Rendering and progress

ModuleResponsibility
progress.pyProgress tracking: reduces untether events into progress snapshots.
markdown.pyMarkdown formatting for progress/final messages; includes helpers like elapsed formatting.
background_status.pyLive background-task block and status message for Claude live sessions.
presenter.pyPresenter protocol: converts ProgressState into transport-specific messages.
transport.pyTransport protocol: send/edit/delete abstractions and message reference types.

Telegram implementation

ModuleResponsibility
telegram/loop.pyMain update loop: polls updates, filters and classifies messages, dispatches built-in commands and runs, applies config hot-reload.
telegram/bridge.pyTelegramPresenter, TelegramBridgeConfig and TelegramTransport, plus the cancel/send helpers (run_main_loop here delegates to loop.py).
telegram/parsing.pyParses Bot API updates into incoming messages and callbacks.
telegram/client.py, telegram/client_api.py, telegram/outbox.pyTelegram API wrapper, the pooled httpx Bot API client, and the paced outbox with retry semantics.
telegram/chat_sessions.py, telegram/topic_state.py, telegram/chat_prefs.pyPersisted chat sessions, forum-topic bindings and sessions, and per-chat preferences.
telegram/render.pyTelegram markdown rendering and trimming.
telegram/onboarding.pyInteractive setup and setup validation UX.
telegram/commands/*In-chat command handlers (/agent, /file, /topic, /ctx, /new, …).
telegram/outbox_delivery.pyAgent-initiated file delivery: scan outbox, send files as Telegram documents, cleanup.
telegram/progress_persistence.pyActive progress message persistence for orphan cleanup on restart.
telegram/steer.py, telegram/followup_mode.py/steer / /queue and the per-chat follow-up mode (Claude live sessions).
telegram/files.pyFile-transfer path rules, including deny-glob matching shared by /file, outbox and /browse.
telegram/voice.pyVoice-note transcription, including the default vocabulary hint.
telegram/reply_context.pyAppends the replied-to message and any selected quote to the prompt as bounded, clearly marked context (#736).
telegram/budget_notice.pyTelegram side of the daily budget gate: the “Daily budget reached” notice and its one-shot Run anyway button (#896).
telegram/approval_originator.pyOpt-in approval_originator_only: only the person who started a run can answer its approval buttons and questions (#388).
telegram/engine_defaults.py, telegram/engine_overrides.pyPer-message engine resolution (topic → chat → project → global defaults) and the per-chat engine overrides (/model, /reasoning, /planmode, Loop mode), incl. the one-shot auto → plan-auto migration.
telegram/topics.py, telegram/listen_mode.pyForum-topic scope helpers (which topic /new and /cancel act on) and the per-chat listen mode (all / mentions).
telegram/offset_persistence.py, telegram/state_store.pyPersisted Telegram update_id offset (last_update_id.json) and the versioned JSON state store behind the chat/topic state files.

Plugins

ModuleResponsibility
plugins.pyEntrypoint discovery and lazy loading (capture load errors, filter by enabled list).
engines.pyEngine backend discovery and loading via entrypoints.
transports.pyTransport backend discovery and loading via entrypoints.
commands.pyCommand backend discovery and loading via entrypoints; command execution helpers.
ids.pyShared ID regex and collision checks for plugin ids and Telegram command names.
api.pyPublic plugin API boundary (untether.api re-exports).

Runners and schemas

ModuleResponsibility
runners/*Engine runner implementations (Claude Code, Codex, OpenCode, Pi, and the deprecated Gemini CLI and Amp).
runners/run_options.pyPer-run options (model, reasoning, permission mode) and the Claude permission-mode tables.
runners/extra_args_guard.pyRejects approval- and sandbox-bypass flags in extra_args.
runners/tool_actions.pyShared tool-call → action kind and title mapping used by the runners’ event translation.
schemas/*msgspec schemas / decoders for engine JSONL streams.

Triggers

ModuleResponsibility
triggers/manager.pyMutable holder for crons + webhooks; hot-reload on TOML change; master pause() / resume() / is_paused toggle.
triggers/server.pyWebhook HTTP server (aiohttp); returns 503 triggers paused while master pause is active; /health reflects paused state.
triggers/dispatcher.pyRoutes webhook/cron fires to run_job() or non-agent action handlers.
triggers/cron.pyCron expression parser, timezone-aware scheduler loop.
triggers/history.pyPersistent JSON history of cron/webhook fire times for /stats triggered/manual breakdown.
triggers/describe.pyHuman-friendly cron rendering for /ping, /config → ⏰ Triggers.
triggers/actions.py, triggers/fetch.pyNon-agent webhook actions (file_write, http_forward, notify_only) and cron fetch steps.
triggers/auth.py, triggers/ssrf.pyWebhook authentication and the SSRF guard for outbound trigger requests.
triggers/rate_limit.py, triggers/templating.pyToken-bucket rate limiting for webhooks and prompt templating from webhook payloads.
triggers/run_once_state.pyPersists fired and pending run_once crons (run_once_fired.json, run_once_pending.json) so one-shots never re-fire and a failed announce is retried.

Configuration and persistence

ModuleResponsibility
settings.pyLoads untether.toml (TOML + env), validates with pydantic-settings.
config.pyRaw TOML read/write (merge/update without clobbering extra sections) and project config types.
config_watch.pyWatches untether.toml and triggers hot-reload (when watch_config = true).
config_reload_notification.pyFormats the Telegram notice after a reload (“No restart needed” / “Restart required”).
service_manager.py, sdnotify.pyDetects the systemd unit or launchd label running Untether so restart notices name the right service (#927); systemd readiness / stopping notifications (READY=1, STOPPING=1).
lockfile.pySingle-instance lock (untether.lock) so two processes can’t poll the same bot.
runtime_loader.pyBuilds the runtime (engines, router, projects) from settings at startup and on reload.
config_migrations.pyOne-time edits to on-disk config (e.g. legacy Telegram key migration).

Utilities

ModuleResponsibility
utils/env_policy.pyEngine subprocess environment allowlist ([security] env_extra_allow / env_extra_prefix_allow).
utils/env_audit.pyChecks a running engine’s real environment against that allowlist.
utils/usage_cache.pyShort-lived cache for the Claude subscription-usage fetch behind /usage and the quota footer.
utils/error_display.py, utils/json_state.pyUser-facing error sanitisation; atomic JSON writes for state files.
utils/paths.pyPath/command relativisation helpers.
utils/streams.pyAsync stream helpers (iter_bytes_lines, stderr draining).
utils/subprocess.pySubprocess management helpers (terminate/kill best-effort).
utils/proc_diag.pyProcess diagnostics for stall analysis (CPU, RSS, TCP, FDs, children).
Was this helpful?

Related Articles