Architecture¶
Core stance¶
The local SQLite projection and CLI are the core of this package.
The optional daemon is still present for testing or local HTTP workflows, but it is no longer required in the preferred call-baxter integration model. In that model, Telegram updates can enter through call-baxter, and tg-agent-cli is used as the projection/index layer only.
Layers¶
api/
Telegram Bot API adapter and generated Telegram request/response models
ingest/
update normalization, media extraction, and persistence policy
store/
SQLite schema and small DB helpers
services/
query services, outbound action services, polling service, hook registry
cli/
pydantic-settings command tree
daemon/
FastAPI application exposing the same service operations over HTTP
Division between CLI and daemon¶
CLI¶
The CLI is optimized for:
- local inspection of already ingested state
- shell automation
- ref-based outbound actions
- manual polling or schedule management
- reproducible examples of modern
pydantic-settingsCLI composition
daemon¶
The daemon remains useful for:
- local debugging of projection services over HTTP
- temporary integration before
call-baxteris introduced - service-style deployments that still want an HTTP wrapper around the projection DB
But the package no longer requires a daemon to stay useful.
Polling model¶
Polling is represented explicitly instead of being hidden in a worker script.
tg_poll_schedules
desired cadence and filters
tg_poll_runs
execution evidence
tg_poll_schedule_claims
cross-process execution ownership with expiring leases
PollingService
live getUpdates call + normalization + hook emission
The scheduler model is intentionally simple:
- schedules have
interval_seconds,timeout_seconds,limit,allowed_updates,hook_names, andsource run_due_schedules()is the primitive loop- external supervisors can call that periodically
- each due run is claimed atomically in SQLite, preventing two daemon requests or processes from running the same schedule concurrently
- the active worker renews its claim while
getUpdatesand normalization run; an expired claim can neither be revived nor completed, and lease loss is recorded on the poll run - the next execution is scheduled from actual completion time rather than batch-start time, avoiding immediate reruns after a long poll
- hook names and Telegram polling bounds are validated before schedule persistence or API execution
This keeps the backend fixed and testable while leaving deployment policy outside the core library.
Projection ordering¶
Telegram update rows remain an appendable audit history, while each normalized message row represents the newest known message version. Message upserts compare edit_date and date; a delayed original-message retry cannot replace a newer edit or its entity/media projections.
Atomic projection visibility¶
Ingestion is split into two phases:
- Telegram file metadata and bytes are fetched before a SQLite write transaction starts.
- The update, chat, users, message, entities, callbacks/reactions, and media rows commit together in one
BEGIN IMMEDIATEtransaction.
File-mode media is downloaded into a unique .part file under a deterministic identity directory. A pre-existing file is reused only when a committed projection row already references it; otherwise each worker prepares its own copy. The prepared file is atomically renamed during the projection transaction. If any later row write fails, SQLite rolls back and cleanup removes only the exact filesystem inode finalized by that worker, and only after confirming that no committed row references the path. After a successful edit, old unreferenced local media files are removed. Housekeeping failures are conservative and do not turn an already committed ingest into a false failure. This avoids holding a database write lock during network I/O while making projection rows atomically visible.
HTTP client lifetime¶
The daemon lazily creates at most one live and one offline Telegram API client and closes them during application shutdown. The Call Baxter Telegram plugin owns and closes the temporary clients it creates for polling, actions, and projection work.
Media policy¶
Media persistence is normalized into tg_message_media and controlled by MediaPolicy.
storage_mode = skip
metadata only
storage_mode = file
bytes written under media base dir; DB links to local_path
storage_mode = blob
bytes stored in SQLite BLOB column
Default scaffold policy is file, because it keeps the DB browse-friendly while avoiding unbounded database growth for larger assets.
WAL mode is established during serialized schema initialization with bounded retries. Normal connections do not change journal mode, which avoids concurrent-startup lock races.
Hooking point¶
The scaffold does not ship a full callback DSL yet. Instead it offers a smaller extension point:
That gives later services a stable place to subscribe to polled update batches without entangling the core browse model with arbitrary callback semantics.