# kcode-tg-kennedy-bot
`kcode-tg-kennedy-bot` is the in-process Telegram transport for a host application. It long-polls Telegram, stores private and group transport state in SQLite, enforces fail-closed group security, and exposes that behavior through a cloneable Rust `Service`. It does not start an HTTP server.
## Lifecycle
The host constructs a `Config` with:
- `database`: SQLite storage path;
- `bot_token`: optional validated `BotToken`;
- `identity_sink`: application-owned identity and whitelist integration;
- `max_voice_bytes`: nonzero byte limit for every inbound and outbound media payload.
`open(config)` opens and migrates storage, initializes group-session cursors, builds the Telegram client, and validates a configured token with `getMe`. It returns `Runtime`. Obtain the cloneable service with `Runtime::service()` before moving the runtime into `Runtime::run()`.
The runtime owns long polling only. A disabled runtime waits indefinitely so it can participate in the host's normal task supervision without creating a listener. The service owns durable state operations and outbound Telegram calls. A configured token is redacted from `Debug`, never persisted or serialized, and zeroized on drop.
`migrate_storage(path)` performs only idempotent SQLite migration.
## Service surface
Status and queue discovery:
- `status`
- `list_private_sessions`
- `list_events`
- `list_group_ingress`
- `list_group_session_updates`
Delivery:
- `send_cold_private_message`
- `send_cold_private_attachment`
- `send_private_message`
- `send_private_attachment`
- `send_group_message`
- `send_group_attachment`
- `send_event_attachment`
- `reply_event`
Event state:
- `bind_event`
- `save_transcription`
- `abort_event`
- `interrupt_event`
- `complete_reset`
Group state:
- `complete_group_ingress`
- `detach_group_session`
- `acknowledge_group_session_context`
- `complete_silent_group_reset`
- `save_group_message_preparation`
Media:
- `event_media`
- `event_media_metadata`
- `group_message_media`
- `group_message_media_metadata`
`send_cold_private_message` and `send_cold_private_attachment` deliver to an established private chat without reading or changing its current conversation binding. The existing private methods retain their compare-and-swap binding semantics, while `reply_event` and `send_event_attachment` remain the bound active-event delivery path.
`list_private_sessions` returns `Vec<PrivateSession>` with typed `telegram_user_id` and `current_conversation_id` fields. Other queue and state payloads remain `serde_json::Value` where the transport projection is heterogeneous and already consumed as a dynamic conversation context. Method arguments, attachment bytes, media results, errors, lifecycle, and status are typed. `Error::code()` exposes stable programmatic categories and `Error::message()` exposes the safe host-facing explanation.
## Host boundary
The transport has no inbound network surface. It does not accept CORS origins, parse routes, expose bearer tokens, or bind port 4324. The embedding application is responsible for its own public API and may report `Service::status()` through its own health endpoint. An HTTP adapter, if a different process ever genuinely needs one, belongs outside this crate and requires an explicit authenticated authority boundary.
The only HTTP performed by the library is communication with Telegram's Bot API. Finite Telegram operations retry only plausibly transient provider, network, timeout, and download failures, with at most five total attempts. `RetryAfter` is respected and permanent errors stop immediately. A send can be duplicated if Telegram accepts it but its response was lost; there is deliberately no durable delivery outbox or exactly-once claim.
The exact-pinned `kcode-telegram-request-policy` 0.1.0 dependency owns those finite request/download retry decisions and Telegram, download, and standard-I/O error classes. The bot-local compatibility facade retains SQLite-specific classes so existing log labels and call sites remain unchanged.
The exact-pinned `kcode-telegram-native-media` 0.1.0 dependency owns Telegram message-media classification, deterministic photo-rendition selection, MIME and filename fallbacks, media capability lists, returned-message duration extraction, and the six native send-method mappings. A small bot-local compatibility facade preserves the existing call sites.
The exact-pinned `kcode-telegram-update-dispatch` 0.1.0 dependency owns bounded keyed FIFO dispatch, per-key serialization, independent-key concurrency, global concurrency, queue saturation, and processor error or panic isolation. A small bot-local compatibility facade binds that generic dispatcher to Telegram updates and preserves safe local failure logging.
The bot continues to own durable polling cursor advancement, update classification and processing, downloads and byte limits, persistence and migrations, identity and group security, sessions and batching, archival, delivery reconciliation, and host lifecycle.
## Identity and group security
The host is authoritative for identity observations, numeric-user whitelist membership, handle pinning, `/adduser` authorization, and application-owned roots. The transport stores no whitelist table and makes authorization decisions using stable numeric Telegram user IDs.
Groups have random opaque IDs that survive Telegram basic-group to supergroup migrations. The permanent group ledger retains every human identity observed in membership updates, joins, leaves, kicks, administrator lists, and message envelopes.
A group is eligible only when:
1. Telegram confirms that the bot is an administrator or owner.
2. Telegram's member count matches the observed active-human ledger plus the bot.
3. Every human ever recorded for the group is in the current host whitelist.
Failure quarantines the group before invocation parsing, message archival, feedback, or media download. Eligibility is recomputed on later updates. Because Telegram cannot enumerate all ordinary members of an existing group, reliable strict onboarding starts with a new group, promotes the bot, and then adds humans so their joins are observed.
Cold group delivery uses the opaque group ID and rechecks the complete security predicate for every send. A chat migration or eligibility change during validation fails closed. Cold sends do not create a conversation, transcript row, or durable outbox.
## Sessions and reconciliation
Private users and each `(group ID, Telegram user ID)` pair have independent conversation pointers. Binding is compare-and-swap guarded. Reply, reset, timeout, attachment completion, context acknowledgement, and orphan detachment verify the expected binding before changing durable state.
Ordinary private messages are gathered until the stream has been quiet for 20 seconds. An eligible group with exactly one active human is invoked by every message from that human and uses the same quiet-window behavior without losing its distinct group session or root. Each newly accepted message restarts the full interval. `list_events` withholds a pending batch until its durable `batch_ready_at` deadline, then returns its last event as the reply target and includes every ordered source item in `batchedEvents`. Binding, reply, attachment completion, interruption, timeout, and edit invalidation apply to the whole batch. Larger groups still require a mention, bot command, reply to Kennedy, or reset and expose those invocations immediately.
Reset commands remain immediate singleton control events rather than joining a conversational batch. Existing events from before the batching migration also remain immediately eligible, so an upgrade cannot strand accepted work.
`detach_group_session` clears only the exact current `(group ID, Telegram user ID, conversation ID)` pointer. Missing, already-detached, or rebound state returns `state_conflict`; messages, events, cursors, resets, membership, and other users are preserved.
Telegram acceptance followed by a local archive or compare-and-swap conflict is an ambiguous external side-effect boundary. The host must reconcile current state rather than blindly retry an effectful delivery.
## Media
Authorized input accepts voice notes, documents, photos, videos, animations, audio, video notes, and stickers. Generic documents stay `document` regardless of MIME type. The size limit is checked before download when Telegram supplies a size and while streaming.
Telegram photos are provider renditions. The transport keeps the rendition with greatest pixel area, then declared size, then stable provider order; it does not claim this is the sender's original upload. Classification and rendition choice are supplied by the exact-pinned native-media dependency; the bot retains provider file download and bounded storage.
`Media` contains exact stored bytes and the resolved media type. Metadata methods expose size and type when callers do not need bytes.
`Attachment` contains:
- `bytes`: required, nonempty, bounded payload;
- `file_name`: optional path-free, control-character-free name of at most 255 characters;
- `media_type`: optional bounded content type;
- `kind`: optional native kind (`photo`, `video`, `animation`, `audio`, `video_note`, or `sticker`); absence means generic document;
- `caption`: optional exact caption.
Captions are limited to 1024 UTF-16 code units and are not accepted for video notes or stickers. Native kinds map to their matching Telegram methods and never silently fall back to `sendDocument`.
Event attachments require the active conversation ID and an explicit `complete` choice. `complete=false` leaves the event active for later files or text; `complete=true` completes only after Telegram accepts the attachment and the binding still matches. Private delivery also carries the expected current conversation pointer. Group delivery resolves and reauthorizes its opaque group ID internally.
`interrupt_event` completes an in-progress event with the `user_stopped` reason while retaining its exact private or group conversation binding. It sends no Telegram message and cannot detach or replace a session.
## Polling, dispatch, and persistence
The next polling offset is durable. Updates are sorted, offered to bounded per-principal dispatch, and then durably skipped past even when an individual update is malformed, saturated, or fails processing. Failure to persist the cursor pauses polling.
Private work is keyed by numeric user ID. Ordinary group work is keyed by Telegram chat ID and sender ID; group-control work has its own key. The dispatch dependency keeps each key FIFO, permits independent keys to execute concurrently, and bounds both queue occupancy and global processing concurrency. The bot's callbacks, Telegram requests, downloads, and retry sleeps run without the SQLite mutex.
SQLite stores Telegram events and media, durable batch identities and quiet deadlines, session pointers, polling cursor, opaque group IDs and aliases, the permanent member ledger, bounded working context, reset work, and background-ingress batches. Completed ingress payloads and group messages no live workflow needs are reclaimed. It stores no bot token, application credential, whitelist, prompt, Kmap root, or capability table.
## Text fidelity
User-facing text is trimmed only to test whether it contains non-whitespace. Stored and delivered text remains byte-for-byte unchanged. Long responses are split at Telegram's UTF-16 limit, preserving leading, trailing, and inter-chunk whitespace.